V8中的垃圾回收算法

·  阅读 977

前言

本文内容来自于对《垃圾回收的算法与实现》内容的总结

V8是啥

这个大家都知道,V8 全称是V8 JavaScript Engine,一个用C++写的JavaScript引擎。

垃圾回收又是啥

垃圾回收的英文是 Garbage Collection,简称GC。在代码运行的过程中,所有的数据都会存放在内存空间,如果没有GC,开发者就必须手动进行内存管理,不然总有一天内存会被占满,进而导致进程崩溃,甚至系统崩溃。GC的作用就是让计算机自动帮开发者进行内存管理,把内存中不需要再次使用的垃圾回收掉。

本文讲的只是V8中的垃圾回收算法,而目前已有的垃圾回收算法远不止这几种,而且目前没有完美的垃圾回收算法。

术语解释

指针

GC时,相应的对象会被销毁或者保留。这时候我们可以通过对象的指针去搜寻其他对象。

指针一般指向对象的首地址。其实可以简单理解为JS中的引用地址。

指动态存放对象的内存空间。JS中,对象存放在堆中。

mutator

mutator的实体是“应用程序”

个人理解,在JS的场景下,它指的就是V8引擎。在V8运行的过程中,会不断的去生成对象,修改对象之间的引用关系(即更新了指针)。这些改变就会带来垃圾,这时候就需要GC

活动对象/非活动对象

活动对象指堆中的能被mutator引用的对象。

非活动对象指堆中的不能被mutator引用的对象,即内存垃圾。

在下文中,多次提及的一个名词是根。什么是根呢?术语上就是可以通过mutator直接引用的对象。举个例子

const obj = new Object(); // A对象
obj.child = new Object(); // B对象
复制代码

第一行代码中,我们创建了一个对象A,它的引用地址赋值给了obj。第二行代码中,我们又创建了一个对象,它的引用地址赋值给了obj.child。那么此时堆如下图所示 这里由于我们可以通过全局变量obj找到A,所以A是活动对象,然后我们又可以通过A找到B,所以B也是活动对象。那么这个全局变量obj就是一个根。

分代式垃圾回收

将内存空间中的对象分为两类,新生代和老年代。新创建的对象存放在新生代中,经过新生代中的GC后,某些对象依然存活。将存活了数次的对象当作老年代对象来处理。

新生代对象->老年代对象的过程,我们称之为晋升。

GC的分类

GC有两种类型,保守式和准确式。

保守式GC(Conservative GC)

GC时不能识别一个东西是不是指针时,这个时候的根被称为不明确的根。

举个例子,我们定义了全局变量a和局部变量obja是一个数值,obj是一个指针(引用地址),引用地址跟值a一样,这个时候GC很难分辨出a到底是指针还是值。于是,保守处理,把它当成一个指针,当obj指向的对象应该被垃圾回收时,由于全局变量a的存在,它不会被垃圾回收。这就是保守式GC

window.a = 0x00d0caf0; // 伪代码,当然是会报错的
const obj = new Object(); // 创建了对象,地址为0x00d0caf0
复制代码

在保守式GC的场景下,对象不能够被移动。因为如果移动了对象,意味着对象的引用地址会发生变化,那么上面的obj相应的会重写成移动后的引用地址,与此同时,全局变量a也会被重写,这就非常恐怖了。所以对象不能够被移动。

准确式GC(Exact GC)

顾名思义,准确式GC能够正确识别出哪些内容是值,哪些内容是指针。要实现准确式GC,依赖于编程语言的处理,意味着成本的增加,这里不再赘述。

V8中的GC

V8中实现了准确式GC

GC算法方面采用了分代垃圾回收,结构如下。

GC复制算法(By Cheney)

GC复制算法将内存空间分为FromTo,当From空间占满时,将From空间中的活动对象(划重点)复制到To空间中,非活动对象回收掉,然后FromTo互换。显而易见,FromTo空间的大小要完全一致。

GC复制算法有很多种,比如 Robert R.Fenichel 和 Jerome C.Yochelson研究出来的和 C. J.Cheney 研究出来的。下面介绍的是Cheney研究出来的算法。

算法流程

在Cheney的复制算法中,算法流程如下

初始状态

首先,复制所有从根直接引用的对象,BG。注意,新的B引用了From中的A,新的G还是在引用From中的B(为区分,写作B1)和E

然后,搜索B1,发现引用了A,于是把A复制到To中,同时修正B中的指向。

接着,搜索G,把E复制到To中,并且G指向B1的指针换到了B

最后,搜索AE,发现没有引用的对象,清空From,将FromTo空间互换,复制算法结束。

优点

  • 吞吐量大
  • 不会发生碎片化
  • 没有递归调用函数。cheney的算法中使用迭代的方式进行复制,这意味着没有过多的消耗栈

缺点

  • 内存空间利用率小,很明显,我们每次只能使用一半的内存空间来分配对象
  • 不兼容保守式GC,因为移动了对象

触发时机

From空间没有分块的时候

GC 标记-清除算法

本算法分为两个阶段

  • 标记阶段: 这个阶段会递归遍历堆中所有的活动对象,打上标记
  • 清除阶段: 遍历整个堆中所有对象,把所有没有被标记的对象回收掉,堆越大,回收耗时越长

很明显,经过这两个阶段后,不能利用的内存空得以再次被利用。

优点:

  • 算法实现简单
  • 可以适用于保守式GC的场景

缺点:

多次GC后会导致内存中出现碎片。碎片化的后果是,即使可用内存的总空间够用,也会因为单个空间不够用导致不能够分配内容(这个时候就要用到下面提到的标记-压缩算法了)

假设内存空间一共5KB,下图中A、B、C、D、E各占了1KB,经历一次GC后,B和D被回收,内存空间中剩余2KB,此时分配一个大小为2KB的对象到内存空间中,无法分配,因为剩余的2KB空间并不连续。

触发时机

  • 老年代空间中某一个空间没有分块的时候
  • 老年代空间中分配了一定数量对象的时候(启动新生代GC时会检查)
  • 老年代空间中没有新生代空间大小的分块的时候(这个时候无法保证新生代GC时的晋升)

GC 标记-压缩算法

  • 标记阶段:V8采用深度优先的方式进行标记,即标记了对象,随后会去标记这个对象的子对象。深度优先遍历时,一般采用递归操作,递归时,自然需要用到栈,在V8中,这个栈由V8自行生成。栈所用的空间是新生代的From空间。因为老年代GC之前,必然会执行新生代GC,这个时候From空间是空的,既然空都空了,不如就把栈放在这里,不用白不用xd。

  • 压缩阶段:

压缩前,可以看到老年代空间中存在很多空白小方块,即内存碎片

压缩时,将内存对象按顺序逐个移动到内存空间的前面

压缩后,可以看到空白小方块已经是连续的了,不存在内存碎片

优点

  • 有效利用堆,不会像复制算法那样只能利用半个堆,也不会存在内存碎片

缺点

  • 在算法过程中,要不断的去移动对象,成本相对于其它算法非常之高

触发时机

  • 老年代空间中的碎片到达一定数量的时候

后记

一开始的时候,标题是《垃圾回收算法》,然而。。。了解深了之后发现,除了上面这些,还有引用计数法,增量式垃圾回收,RC Immix算法等等。即使是相同算法,不同设计者也有一定的区别。不同语言的GC算法实现上也有一定的区别。内容多的离谱,于是在标题前面加上了“V8中的”。

这个文涉及的东西实际开发中并不常见,而且术语比较多,难免有地方写错了,有朋友发现了的话,麻烦评论区留个言。

分类:
前端
标签: