乐趣区

关于v8:深入理解之V8引擎的垃圾回收机制

本文谨用于笔者集体了解和总结 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 引擎垃圾回收机制
退出移动版