分代式垃圾回收算法
根据对象的存活时间将内存的垃圾回收进行不同的分代,分别对不同的分代采取更高效的算法。
新生代与老生代
- 新生代中的对象为存活时间较短的对象;
- 老生代中的对象为存活时间较长的对象;
- V8堆的整体大小就是新生代加老生代的内存空间。
Scavenge算法
什么是Scavenge算法呢?
新生代对象主要通过scavenge算法,采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,一个处于正在使用中,另一个处于闲置状态。正在使用中的semispace空间称为Form空间,处于闲置状态的空间称为To空间。当我们分配对象的时候,先是在Form空间中进行分配。当开始垃圾回收的时候,会检查From空间中的存活对象,这些存活对象将会被复制到To空间,而非存活对象将会被释放。完成复制后,Form空间和To空间的角色将会发生对换。
优点?缺点?
- 缺点是空间利用率低,只能使用堆内存中的一半;
- 优点是时间效率高,因为只复制存活的对象,新生代中的对象存活时间短,适合这个算法。
晋升
什么是晋升?
在scavenge过程中,From空间中的存活对象将会复制到To空间中去,然后将Form空间和To空间进行翻转。、From空间中的存活对象在复制到To空间之前需要进行检查。在满足一定条件下,需要将存活周期长的对象移动到老生代中,也就是完成对象晋升。
晋升的条件
- 对象是否经历过Scavenge
- To空间的内存占用比超过限制(25%)
Mark-Sweep算法
对于老生代中存活时间较长的对象采用Mark-Sweep 与 Mark-Compact相结合的方式进行垃圾回收。
什么是Mark-Sweep?
Mark-Sweep(标记清除算法),它分为标记和清除两个阶段。在标记阶段遍历堆中的所有对象,并标记活着的对象,在接下来的清除阶段中,只清除没有被标记的对象。老生代中存活对象多,存活时间长所以采用Mark-Sweep算法对非存活对象进行清除。
具体流程,我们可以这样理解:
在标记前,垃圾回收器会将所有的数据设置为白色,然后回收器从根节点开始遍历,把所有能访问到的数据标记为黑色,遍历结束后,标记为黑色的就是存活的活动对象,而白色就是待清理的非存活对象。
Mark-compact算法
上面提到的标记清除算法会带来一个问题:内存碎片。
在进行一次标记清除回收后,内存空间会出现不连续的状态,对后续的内存分配造成问题。为了解决内存碎片问题,Mark-compact(标记整理)算法被提出来,标记整理算法是在Mark-Sweep(标记清除)算法进行后对存活的对象进行整理,将存活对象往一端移动,移动完成后,直接清理掉边界外的内存。 下面的图片为Mark-Compact完成标记并移动存活对象后的示意图,白色格子为存活对象,深色格子为死亡对象,浅色格子为空闲对象。
全停顿
什么是全停顿?
在垃圾回收的过程中为了保证回收的正确性,需要将JavaScript应用逻辑暂停下来,等到垃圾回收执行完成后再恢复JavaScript应用逻辑,这种行为被称为全停顿。
在新生代和老生代的垃圾回收过程中,V8采用并行回收的机制,也就是说在主线程之外开启多个辅助线程帮助垃圾回收,达到减少回收时间的目的。
但是这也带来了一些问题,并行回收对于新生代能够进行较好的优化,但是老生代中存放的都是存活时间长,容量大的对象,进行一次全停顿会带来明显的卡顿,用户体验并不好。 从2011年开始,就以上问题V8采用增量标记算法(Incremental Marking)。
Incremental Marking
Incremental Marking(增量标记算法),就是将一次完整的垃圾回收拆分为许多小的“步进”,让垃圾回收与应用逻辑交替执行。
暂停的垃圾回收过程该如何无缝衔接般的恢复呢?—— 三色标记法
在Mark-Sweep算法中我们知道可以把对象设置为黑色和白色进行区分,但是在Incremental Marking算法中,垃圾回收器暂停后重新启动,如何找到上次停顿的位置成了问题。为了解决这个问题,V8采用了三色标记法,每个对象都有两个标记位和一个标记工作表来实现标记,标记位的颜色分为白色、灰色、黑色。
- 白色是未标记的对象;
- 黑色是自身和成员变量都被标记的对象;
- 灰色是自身被标记,但是所属的成员变量未被标记。
以下是该算法的执行过程:
- 一开始所有对象都是白色,从根节点开始遍历,先把遇到的这组对象标记为灰色并推入标记工作表中;
- 当回收器从标记工作表中弹出对象并访问它的引用对象时,将其自身由灰色转变为黑色,并将下一个引用对象转为灰色;
- 直到遍历到了尽头,剩下的白色对象就是需要被清理的目标。如果中途遇到了暂停,我们可以通过寻找灰色的对象节点来恢复执行。
写屏障
假设在增量标记的暂停过程中,标记好的对象引用发生了变化该怎么办呢?
如下图所示,在增量标记的暂停过程中,标记好的对象引用发生了变化。将B指向C的引用改成了B指向D,由于B已经是黑色了,所以标记阶段结束前垃圾回收器都不会再对B进行检测,D将一直是白色直到这次GC结束被回收。
所以V8引入了写屏障的机制,每当引用发生变化时,如果有黑色对象指向白色对象,该机制会强制将白色对象改成灰色,从而保证下一次增量标记的正确性。