Javascript V8垃圾回收机制

66 阅读6分钟

JavaScript V8引擎

V8 是驱动 Google Chrome 的 JavaScript 引擎的名称。

V8 提供了 JavaScript 执行的运行时环境。 DOM 和其他 Web 平台 API 由浏览器提供。

V8的内存限制

在64位系统下约为1.4 GB,32位系统下约为0.7 GB。

V8为什么要限制堆的大小?

  • 表层原因:V8最初是为浏览器设计的,不太可能遇到使用大量内存的场景。
  • 深层原因:V8的垃圾回收机制的限制。

V8的对象分配

在V8中,所有的JavaScript对象都是通过堆来进行分配的。

img

当在代码中声明变量并赋值时,所使用对象的内存就分配在堆中。如果已经申请到的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆的大小超出V8的限制为止。

V8的垃圾回收机制

V8的垃圾回收策略主要基于分代式垃圾回收机制,把内存分为新生代和老生代。

分代式垃圾回收机制:按照对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同的分代的内存实施更高效的回收算法。

新生代:新生代中的对象为存活时间较短的对象,采用复制算法(内存一分二,分为From和To)。

老生代:老生代中的对象为存活时间较长或常驻内存的对象,使用标记清除和标记整理算法。

img

新生代中的垃圾回收

img

新生代中的对象主要通过Scavenge算法进行垃圾回收,Scavenge算法在具体实现上采用Cheney算法。

大致过程为:将堆内存一分为二,每一部分空间称为semispace。在这两个semispace中,只有一个空间处于使用中,另一个处于空闲状态。处于使用状态的空间称为From空间,空闲状态的空间称为To空间。分配对象时,现在From空间进行分配。当开始进行垃圾回收时会检查From空间中的存活对象,这些存活对象会被分配到To空间中,而非存活对象占用的空间将被释放。完成释放后,From空间和To空间的角色发生对换。简而言之,就是将存活对象在两个空间中来回复制。

对象晋升

当一个对象经过多次复制依然存活时,将会被认为是生命周期较长的对象。这种对象随后会被移动到老生代中(对象晋升),采用新的算法进行管理。

对象晋升的条件:

  • 对象是否经理过Scavenge回收。
  • To空间的内存用比是否超过限制。

在默认情况下,V8的对象分配主要集中在From空间中。对象从From空间中复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收。如果已经经历过了,会将该对象从From空间复制到老生代空间中,如果没有,则复制到To空间中。

img

另一个判断条件是To空间的内存占用比。当要从From空间复制一个对象到To空间时,如果To空间已经使用了超过25%,则这个对象直接晋升到老生代空间中。之后From和To地位互换。

img

老生代中的垃圾回收

对象晋升后,在老生代中接收新的回收算法处理。

V8在老生代中主要采用Mark-Sweep(标记清除)和Mark-Compact(标记整理)相结合的方式进行垃圾回收。

Mark-Sweep分为两个阶段:标记和清除。

在标记阶段遍历堆中所有对象,并标记活着的对象,在随后的清理阶段中,只清理没有被标记的对象,即只清理死亡的对象。如下图: 黑色部分为标记死亡的对象。

img

Mark-Sweep最大的问题是在进行一次标记清除回收后会造成内存空间的不连续。这种内存碎片会对后续的内存分配造成问题。例如:此时,当需要分配一个比较大的对象时,所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而此时的回收是不必要的。

为解决Mark-Sweep后的内存碎片化问题,Mark-Compact(标记-整理)被提出。Mark-Compact在整理阶段将活的对象向一端移动,移动完成后,直接清理掉边界外的内存。如下图所示,白色格子为存活对象,深色格子为死亡对象,浅色格子为存活对象移动后留下的空洞。

img

在V8的回收策略中,Mark-Sweep和Mark-Compact是结合使用的。主要使用Mark-Sweep,在不足以对从新生代中晋升过来的对象进行分配时才会使用Mark-Compact。

img

Incremental Marking(增量标记)

为了避免出现JavaScript应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的3种基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为“全停顿”(stop-the-world)。

在V8的分代式垃圾回收中,一次小垃圾回收只收集新生代,由于新生代默认配置得较小,且其中存活对象通常较少,所以即便它是全停顿的影响也不大。但V8的老生代通常配置得较大,且存活对象较多,全堆垃圾回收(full 垃圾回收)的标记、清理、整理等动作造成的停顿就会比较可怕,需要设法改善。

为了降低全堆垃圾回收带来的停顿时间,V8先从标记阶段入手, 将原本要一口气停顿完成的动作改为增量标记(Incremental Marking),也就是拆分为许多小“步进”,每做完一“步进”就让JavaScript应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。

img

V8后续还引入了延迟清理(lazy sweeping)与增量式整理(incremental compaction),让清理与整理动作也变成增量式的。同时还计划引入并行标记与并行清理,进一步利用多核性能降低每次停顿的时间。