V8 的垃圾回收机制
我们在使用 javaScript 这门语言开发时,不需要像 C / C++ 开发那样需要时刻关注内存的分配和释放问题,与 Java 一样,由垃圾回收机制来进行自动的内存管理。
虽然前端开发中几乎是很少碰到由垃圾回收导致的性能问题,但是也还是有必要了解一下 V8 引擎的垃圾回收机制。
1.V8 的对象分配
在 V8 中,所有 JavaScript 对象都是通过堆来进行分配的。当我们在代码中声明变量并赋值时,所使用的对象的内存就分配在堆中。如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,知道堆的大小超过 V8 限制。
V8 为何要限制堆的大小
表层原因是因为 V8 最初为浏览器而设计,不太可能遇到用大量内存的场景。对于网页来说 V8 的限制值已经绰绰有余。深层原因是 V8 的垃圾回收机制的限制。按官方的说法,以 1.5 GB 的垃圾回收堆内存为例,V8 做一次小的垃圾回收需要 50 毫秒以上,做一次非增量式的垃圾回收甚至要 1 秒以上。这是垃圾回收中引起 JavaScript 线程暂停执行的时间,这显然是无法接受的。
2.V8 的垃圾回收算法
V8 的垃圾回收策略主要基于分代式垃圾回收机制。没有一种垃圾回收算法能够胜任所有的场景,因为在实际应用中,对象的生存周期长短不一,不同的算法只能针对特定情况具有最好的效果。按照对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同分代的内存施以更高效的算法。
V8 的内存分代
主要将内存分为新生代和老生代两代。新生代的对象存活时间较短,老生代则是较长的。
-
Scavenge 算法
新生代中的对象主要通过 Scavenge 算法进行垃圾回收。在 Scavenge 的具体实现中主要采用了 Cheney 算法。
Cheney 算法是一种采用复制方式实现的垃圾回收算法。将内存一分为二,每一部分空间成为 semispace。使用其中一个,这一个成为 From 空间,另一个闲置的称为 To 空间。当垃圾回收开始时,会检查 From 空间中的存活对象,并将其复制到 To 空间,然后释放掉这个空间。之后 From 和 To 角色互换(翻转)。
Scavenge 只能使用堆内存的一半,但是对于生命周期短的场景存活对象只占少部分,所以在时间效率上有优异的表现,典型的空间换时间的算法,适合应用在新生代。
当一个对象经过多次复制依然存活时,他将被认为是生命周期较长的对象,随后便会呗移动到老生代。
对象从新生代移动到老生代这个过程叫做晋升。
对象晋升的条件主要是两个
- (是否经历过 Scavenge 回收)在从 From 空间复制到 To 空间时,会检查它的内存地址来判断这个对象是否已经经过一次回收。经历过的会呗复制到老生代空间中。
- (To 空间的内存占用比)当复制到 To 空间时,如果 To 空间已经使用了超过 25%,则会被直接晋级到老生代空间。这是因为这个 To 空间将要变成 From 空间,如果占比过高,会影响后续的内存分配。
-
Mark-Sweep & Mark-Compact
对于老生代中的对象,由于存活对象占较大比重,不适合 Scavenge 的方式,复制效率低、浪费一半空间。
Mark-Sweep(标记清除),在标记阶段遍历堆中的所有对象,并标记活着的对象,清除阶段只清除没有被标记的对象。
但是在进行一次标记清除回收后,内存空间会出现不连续的状态。
Mark-Compact(标记整理),在 Mark-Sweep 的基础上演变而来。在标记阶段过后,在整理的过程中,将活着的对象往一端移动,然后清理边界外的内存。
在 V8 中会将这两种方式结合使用。
回收算法 Mark-Sweep Mark-Compact Scavenge 速度 中等 最慢 最快 空间开销 少(有碎片) 少(无碎片) 双倍空间(无碎片) 是否移动对象 否 是 是 V8 主要使用 Mark-Sweep,在空间不足以分配从新生代中晋升过来的对象时才使用 Mark-Compact。
-
Incremental Marking
在执行垃圾回收时,应用逻辑会被暂停,这种行为叫做“全停顿”(stop-the-world)。
在停顿时间过长时,用户就能够感觉到卡顿,为了降低垃圾回收带来的停顿时间,V8 从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记(incremental marking),也就是拆分为许多小“步进”,每做完一个就让 JavaScript 应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行知道标记阶段完成。
V8 还引入了延迟清理(lazy sweeping)与增量式整理(incremental compaction),让清理与整理动作也变成增量式的。
参考文献 [1] 朴灵. (2013). 深入浅出node.js. 人民邮电出版社.