likes
comments
collection
share

一文带你读懂V8垃圾回收机制

作者站长头像
站长
· 阅读数 14

浏览器垃圾的产生

首先,我们要先明白在js中内存垃圾的定义是什么,在js内存管理策略中它会对失去可达性(无引用或者无法通过某种方式进行访问)的空间进行回收。既失去可达性的内存空间会被视为是垃圾。 在正常开发中,我们不可避免的会在程序中进行各种变量声明函数创建等操作,这些操作无疑都是需要从浏览器处进行内存申请,浏览器进行内存分配,但是如果我们在使用引用类型变量时更改了其引用地址,就导致之前被引用的堆地址失去可达性而被白白占用(参考下面代码例子),如果变量变多这种被白白占用的地址会越来越多,导致新的变量能分配使用的内存会越来越小,系统运行速度会越来越慢,达到临界值时会导致系统崩溃。

 let ceshi = {
    a: 1,
    b: 2

  }
  // 上方引用地址无变量引用导致白白占用内存空间
  ceshi = [1, 2, 3, 4, 6]

垃圾回收机制

由上文我们可以知道浏览器在运行程序时就会产生垃圾,但是正常情况我们开发时也从未主动去回收垃圾,那程序为何依旧能正常运作呢。那是因为JavaScript自己内部会定时(实时太消耗性能)通过GC(Garbage Collection)进行垃圾回收。

垃圾回收策略

而在GC中常用的回收策略

标记清除(Mark-Sweep)

标记清除是JavaScript引擎中进行垃圾回收中使用到最多的算法,在目前主流的浏览器厂商中几乎都是可以看到标记清除算法,只不过不同浏览器厂商优化不同,而且不同的浏览器上运行的性能也有差异。 而此算法主要核心分为两部分标记和清除。 在代码执行阶段,为程序中所有的变量添加上一个二进制字符(二进制运算最快)并初始值置为0(默认全是垃圾),然后遍历所有的对象,被使用的变量标记置为1,在程序运行结束时回收掉所有标记为零的变量,回收结束之后将现存变量标记统一置为0,等待下一轮回收开启。

优点

标记清除算法思路清晰,实现比较简单。

缺点

由于系统分配的内存时间不同,回收的先后顺序也是不同的,这时就会导致剩余空闲空间并不是连续的,出现了内存碎片现象。

一文带你读懂V8垃圾回收机制

图片来源于网络

内存碎片化之后,新的分配空间被分配时需要先计算一下满足符合要求的空间,增加了计算负担。同时如果后续系统需要分配的新变量使用空间很大,虽然系统总剩余内存是满足需求,但是并没有连续的满足需求的空间进行分配,这时可能会出现分配失败。

一文带你读懂V8垃圾回收机制

图片来源于网络

所以虽然标记清除算法比较简单但是缺点也是很明显由于内存碎片的诞生导致的分配时间较长和空间浪费,所以只要解决掉内存碎片这个致命问题,这两个问题就会迎刃而解。 这时候 **标记整理** (Mark-Compact)算法闪亮登场,他的清除逻辑和标记清除算法基本相似不过进行了优化,会在清除结束之后讲活着的空间进行整理向一端移动,同时清理掉内存的边界。

引用计数

引用计数算法顾名思义,他的策略就是跟踪记录每个变量值被使用的次数,如果一个引用类型的值给一个声明的变量赋值,则将这个引用类,型的值的引用次数为1,如果同一个值被又被赋值给另一个变量,则引用计数再加1,如果之前被赋值的变量值变更成了其他引用类型,则原本的引用类型引用计数减1,如果这个引用类型的引用计数为0时表示,此时为不可达状态,浏览器垃圾回收器就讲此类型占用的空间进行回收掉(此处是实时的,当计数变为0既会被立即回收)。

优点

1、实时回收,引用计数当归零就立即进行回收操作。 2、不会暂停执行栈,标记清除算法定时进行垃圾回收时会先暂停程序运行,来进行垃圾回收,而引用计数是实时回收不会暂停程序的运行

缺点

1、空间浪费,由于需要进行计数,所以需要开辟空间来存储计数器,同时由于引用无上限故占用空间也是无上限。 2、无法解决循环引用无法回收(致命问题),循环引用既两个引用类型AB,A有一个地址指向了B,B也有一个对象指向了A,导致两者引用技术为2,正常情况下当test函数运行结束进行垃圾回收,但是AB两者的基数都是不是0则回收失败,无法清除,这种情况大量发生时会造成大量的内存空间被浪费,故引用计数算法现在已经很少使用逐渐被标记清除算法替代。

  function test() {
    let A = new Object()
    let B = new Object()
    B.a = A
    A.b = B
  }

V8对GC的优化

分代式优化

之前GC的清除算法无论是标记清除还是标记整理,在进行回收时都需要检查内存中的所有对象,但是如果存在一些,体积大,存活时间长,创建早的内存来进行检查,相当于是做了无用功,而新创建,体积小和存活时间短的对象需要更加频繁的检查所以基于这个问题V8提出了新生代和老生代的优化策略。将内存空间划分为新生代和老生代两个部分,不同部分执行不同的回收策略。

一文带你读懂V8垃圾回收机制

图片来源于网络

新生代

顾名思义新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持 1~8M 的容量。 而新生代中的内存又会被拆分为两部分,使用区和空闲区,浏览器进行内存申请时分配使用区空间,当使用区空间快被写满时则进行一次垃圾回收,新生代的垃圾回收器会对使用区的活动对象进行标记,标记完成之后将使用区活跃的对象复制到空闲区,并进行排序,随后进入垃圾清理阶段,对使用区进行清理,清理操作完成之后,使用区和空闲区进行角色互换,之前的空闲区变成新的使用区,之前的使用区变成新的空闲区,循环往复。

当一个对象被多次复制还未被清理掉,故此对象会被认定为生命周期较长的对象,会被从新生代移动到老生代中,采用老生代的垃圾回收机制管理。

不过还需要注意一个特殊情况,如果新生代的复制一个对象到空闲区,如果空闲区的使用空间超过25%之后这个对象会被立即复制到老生代,而25%的红线要求是为了保证进行空闲区和使用区翻转时对于新的对象分配空间操作不会被影响。

老生代

相比于新生代,老生代顾名思义存放的就是一些生命周期比较长,经过多次新生代垃圾回收还存在的对象,同样的相比于新生代不仅垃圾回收频率较低,存储空间也是比新生代大的多。而老生代的回收算法就比较简单就是标记清除算法,不过在v8中为了处理标记清除算法产生的内存碎片问题,使用了标记整理算法进行空间优化大大提高了回收效率。

并行回收

众所周知JavaScript是一门单线程语言,所以在进行GC回收时会阻塞js脚本的运行导致系统停顿,等GC回收结束后恢复运行,这被称为全停顿。

但是这样的话会存在极大的风险,如果GC回收时间较长,就会导致系统停顿时间较长这是不可被接受的。所以V8引擎加入了并行回收的优化机制,在开启GC回收线程之后,会同时开启多个辅助线程进行处理,提高回处理时间,虽然增加了一部分线程之间协调的时间,但是总时间比一个线程用时来讲大大的缩短。避免系统卡顿时间过长。

增量标记

由于全停顿标记策略在处理老生代垃圾回收时即使是有并行处理优化但是消耗时间也会消耗大量的时间,所以在2011年时V8团队又提出了增量标记策略来进行优化。 增量标记思想就是将一次GC标记过程进行拆分,一次执行一小部分,执行完毕后继续执行脚本,执行一段脚本之后又继续执行刚刚拆分的GC标记任务,循环往复直至这次GC标记完成。

三色标记法(恢复与暂停)

在引入三色标记法之前的GC标记只是将活动的变量标记为黑色,不活动的变量标记为白色,当GC标记过程结束之后,系统会回收掉所有的白色标记变量,但是这种非黑即白的方法虽然清除起来非常方便但是存在一个问题执行一段时间之后无法知道执行到了哪里,不能进行暂停。所以V8又引入了一个灰色进行暂停和恢复操作。

一文带你读懂V8垃圾回收机制

图片来源于网络

如图所示,在GC标记开始时所有对象都是白色的,然后从根对象开始进行标记,先将这组对象标记为灰色然后进行记录,如果此时进行中断,后续恢复时既从灰色标记时开始即可,当回收器从标记工作表中弹出对象并访问他们的引用对象时,会将灰色置为黑色,同时将下一个引用对象置为灰色,继续往下进行标记工作。直至无可标记为灰色对象为止,此时表示GC标记过程结束,将所有未标记的变量进行回收工作。所以三色标记法可以渐进执行而不用每次执行都要全盘进行扫描整个内存空间,可以配合增量回收减少全停顿时间,提升体验。

写屏障

在一次完成GC标记暂停中,如果执行任务程序时内存中存在的变量引用关系被改变了,这样会导致此次GC存在问题。所以V8团队提出了写屏障作为保护。

一文带你读懂V8垃圾回收机制

图片来源于网络

如图所示,现有A、B、C三个对象依次被引用,且在GC过程中已经被标记了,但是在暂停GC任务,插入执行程序任务之后,引用关系被改变了,新增了一个新变量D,但是此时程序中也未存在灰色标记的变量,下一步进行清除机制时,新变量D按清除机制来讲是要被清除掉,但是这是极其不合理的,一个新的变量还存在引用就被回收掉,这会导致程序云行报错。此时写屏障机制就派上用场了,一旦有黑色的对象引用白色的对象,就会强制将被引用的白色变量标记为灰色,保证下一次的增量GC正确运行,这个机制称为强三色不变性(白色变量D被黑色变量B引用之后会被强制置灰保证程序运行正确性)。

惰性清理

在增量GC标记之后下一步就是来真正回收内存空间,通过惰性清理来进行清除释放内存。惰性清理机制运行原理是在进行回收时如果内存足够就可以将这个回收清理时间稍微延迟一下,让JavaScript脚本先执行,清理时也不会一下全部清理掉所有的垃圾,会根据按需进行清理直至所有垃圾都回收完毕,然后继续等待下个GC标记阶段执行结束。

并发回收

虽然增量标记和惰性清理的出现使主线程停顿时间大大减少了,但是总体的停顿时间其实并未减少,如果真正细算起来甚至还增加了,应用程序的吞吐量也被降低,不过用户和浏览器的交互体验大大提升牺牲也是值得的。但是后续V8团队为了使回收更加高效, 又使用了并发回收机制,他是在主线程在执行程序任务时,主动开启辅助线程进行GC回收。而主线程又可以自由执行而不会挂起(标记操作全部由辅助进程操作)。

一文带你读懂V8垃圾回收机制

图片来源于网络

总结

综合上文的知识点V8引擎的垃圾回收机制也逐渐揭开:分代式策略是V8里面垃圾回收机制的中流砥柱, 只不过老生代的垃圾回收略显麻烦为了优化性能和体验开发团队煞费苦心,在GC标记阶段通过并发回收策略开启辅助线程完成标记操作,清理操作时通过增量任务和惰性回收策略进行清理(同时也会开启辅助线程进行清理)大大提升了回收效率提高了人机交互的体验感。不得不说当下web式应用程序能够飞速发展少不了V8引擎的一份力。

后话

这段时间随着对前端知识系统的回顾学习,发现前端知识的海洋如此广阔,之前只拘泥自己工作业务那一偶真的是坐井观天,不过还好,现在学习还来得及。愿诸君共勉,一起早日实现自己心中所想。