本文谨用于笔者集体了解和总结V8引擎的垃圾回收机制,本文次要参考一文搞懂V8引擎的垃圾回收

在理解V8垃圾回收机制之前,咱们先来论述一些概念:

「全进展」:垃圾回收算法在执行前,须要将应用逻辑暂停,执行完垃圾回收后再执行应用逻辑。

如果一次GC须要50ms,应用逻辑就会暂停50ms。为什么会暂停呢?

①因为js是单线程执行的,进入垃圾回收后,js应用逻辑须要暂停,以留出空间给垃圾回收算法运行。
②垃圾回收其实是十分耗时间的操作。

V8引擎垃圾回收策略:

  • V8的垃圾回收策略次要是基于分代式垃圾回收机制,其依据对象的存活工夫将内存的垃圾回收进行不同的分代,而后对不同的分代采纳不同的垃圾回收算法。
  • 在新生代的垃圾回收过程中次要采纳了Scavenge算法;在老生代采纳Mark-Sweep(标记革除)Mark-Compact(标记整顿)算法。

在理解新生代和老生代的垃圾治理算法之前,咱们无妨先来理解一下V8引擎垃圾治理的内存构造;

V8引擎垃圾治理的内存构造:

  • 新生代(new_space):大多数的对象开始都会被调配在这里,这个区域绝对较小然而垃圾回收特地频繁,该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将须要保留的对象复制过去。
    (笔者看到了两种分区说法:①From区:To区=1:1②From区:To区:To区=8:1:1,本文仅用来理解回收机制,不对此处过多探讨)
  • 老生代(old_space):新生代中的对象在存活一段时间后就会被转移到老生代内存区,绝对于新生代该内存区域的垃圾回收频率较低。老生代又分为老生代指针区和老生代数据区,前者蕴含大多数可能存在指向其余对象的指针的对象,后者只保留原始数据对象,这些对象没有指向其余对象的指针。
  • 大对象区(large_object_space):寄存体积超过其余区域大小的对象,每个对象都会有本人的内存,垃圾回收不会挪动大对象区。
  • 代码区(code_space):代码对象,会被调配在这里,惟一领有执行权限的内存区域。
  • map区(map_space):寄存Cell和Map,每个区域都是寄存雷同大小的元素,构造简略。

新生代区:

新生代区次要采纳Scavenge算法实现,它将新生代区划分为激活区(new space)又称为From区未激活区(inactive new space)又称为To区
程序中生命的对象会被存储在From空间中,当新生代进行垃圾回收时,处于From区中的尚存的沉闷对象会复制到To区进行保留,而后对From中的对象进行回收,并将From空间和To空间角色对换,即To空间会变为新的From空间,原来的From空间则变为To空间。

因而,该算法是一个就义空间来换取工夫的算法。

基于上述算法,算法图解实现如下(转载):

  1. 假如咱们在From空间中调配了三个对象A、B、C
  2. 当程序主线程工作第一次执行结束后进入垃圾回收时,发现对象A曾经没有其余援用,则示意能够对其进行回收
  3. 对象B和对象C此时仍旧处于沉闷状态,因而会被复制到To空间中进行保留
  4. 接下来将From空间中的所有非存活对象全副革除
  5. 此时From空间中的内存曾经清空,开始和To空间实现一次角色调换
  6. 当程序主线程在执行第二个工作时,在From空间中调配了一个新对象D
  7. 工作执行结束后再次进入垃圾回收,发现对象D曾经没有其余援用,示意能够对其进行回收
  8. 象B和对象C此时仍旧处于沉闷状态,再次被复制到To空间中进行保留
  9. 再次将From空间中的所有非存活对象全副革除
  10. From空间和To空间持续实现一次角色调换

对象降职:

当一个对象在通过屡次复制之后仍旧存活,那么它会被认为是一个生命周期较长的对象,在下一次进行垃圾回收时,该对象会被间接转移到老生代中,这种对象从新生代转移到老生代的过程咱们称之为降职
对象降职的条件次要有以下两个(满足其一即可):

  • 对象是否经验过一次Scavenge算法
  • To空间的内存占比是否曾经超过25%

默认状况下,咱们创立的对象都会调配在From空间中,当进行垃圾回收时,在将对象从From空间复制到To空间之前,会先查看该对象的内存地址来判断是否曾经经验过一次Scavenge算法,如果地址曾经产生变动则会将该对象转移到老生代中,不会再被复制到To空间。
流程图示意:

如果对象没有经验过Scavenge算法,会被复制到To空间,然而如果此时To空间的内存占比曾经超过25%,则该对象依旧会被转移到老生代,如下图所示:

之所以有25%的内存限度是因为To空间在经验过一次Scavenge算法后会和From空间实现角色调换,会变为From空间,后续的内存调配都是在From空间中进行的,如果内存应用过高甚至溢出,则会影响后续对象的调配,因而超过这个限度之后对象会被间接转移到老生代来进行治理。

老生代区:

在解说老生代Mark-Sweep(标记革除)Mark-Compact(标记整顿)算法之前,先来回顾一下援用计数法:对于对象A,任何一个对象援用了A的值,计数器+1,援用生效时计数器-1,当计数器为0时指责回收,然而会存在循环援用的状况,可能会导致内存透露,自2012年起,所有的古代浏览器均放弃了这种算法。

function foo() {//循环援用样例    let a = {};    let b = {};    a.a1 = b;    b.b1 = a;}foo();

Mark-Sweep(标记革除)算法:
Mark-Sweep(标记革除)分为标记和革除两个阶段,在标记阶段会遍历堆中的所有对象,而后标记活着的对象,在革除阶段中,会将死亡的对象进行革除。Mark-Sweep算法次要是通过判断某个对象是否能够被拜访到,从而晓得该对象是否应该被回收,具体步骤如下:

  1. 垃圾回收器会在外部构建一个根列表,用于从根节点登程去寻找那些能够被拜访到的变量。比方在JavaScript中,window全局对象能够看成一个根节点。
  2. 垃圾回收器从所有根节点登程,遍历其能够拜访到的子节点,并将其标记为流动的,根节点不能到达的中央即为非流动的,将会被视为垃圾。
  3. 垃圾回收器将会开释所有非流动的内存块,并将其归还给操作系统。

    然而通过标记革除之后的内存空间会⽣产很多不间断的碎⽚空间,这种不间断的碎⽚空间中,
    在遇到较⼤的对象时可能会因为空间不⾜⽽导致⽆法存储。
    为了解决内存碎⽚的问题,须要使⽤另外⼀种算法:标记-整顿(Mark-Compact)。

标记-整顿(Mark-Compact):
标记整顿看待未存活对象不是⽴即回收,⽽是将存活对象挪动到⼀边,而后间接清掉端边界以外的内存。
这里为了便于了解,援用两个流程图。

  1. 假如在老生代中有A、B、C、D四个对象
  2. 在垃圾回收的标记阶段,将对象A和对象C标记为流动的
  3. 在垃圾回收的整顿阶段,将流动的对象往堆内存的一端挪动
  4. 在垃圾回收的革除阶段,将流动对象左侧的内存全副回收


至此就实现了一次老生代垃圾回收的全副过程,然而因为前文提到的「全进展」的存在,在标记阶段同样会妨碍主线程的执行,一般来说,老生代会保留大量存活的对象,如果在标记阶段将整个堆内存遍历一遍,那么势必会造成重大的卡顿。因而,V8引擎有引入了Incremental Marking(增量标记)的概念。
Incremental Marking(增量标记)

将本来须要一次性遍历堆内存的操作改为增量标记的形式,先标记堆内存中的一部分对象,而后暂停,将执行权从新交给JS主线程,待主线程工作执行结束后再从原来暂停标记的中央持续标记,直到标记残缺个堆内存。
即:把垃圾回收这个⼤的工作分成⼀个个⼩工作,穿插在 JavaScript工作两头执⾏

这个理念其实有点像React框架中的Fiber架构,只有在浏览器的闲暇工夫才会去遍历Fiber Tree执行对应的工作,否则提早执行,尽可能少地影响主线程的工作,防止利用卡顿,晋升利用性能。

得益于增量标记的益处,V8引擎后续持续引入了提早清理(lazy sweeping)和增量式整顿(incremental compaction),让清理和整顿的过程也变成增量式的。同时为了充分利用多核CPU的性能,也将引入并行标记和并行清理,进一步地缩小垃圾回收对主线程的影响,为利用晋升更多的性能。
最初附上V8-GC的触发机制:

参考文献及图片出处:

  • 一文搞懂V8引擎的垃圾回收
  • Node —— V8 GC 浅析
  • 浅谈V8引擎垃圾回收机制