浏览器原理之V8引擎的垃圾回收机制

145 阅读6分钟

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

在node环境,可以通过下面指令查看内存使用的情况。

image.png

V8对堆的大小是有限制的,node在启动时可以传递--max-old-space-size(单位:MB)来调整内存限制的大小。

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

image.png

当我们把数量级调大,并把老生代的大小设置为16MB时,会导致溢出。

image.png

堆的大小限制原因

深层原因是V8的垃圾回收机制限制。按官方的说法,以1.5GB的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至熬1秒以上。这是垃圾回收中引起JS线程暂停执行的时间,在这样的时间花销下,应用的性能和响应能力都会直线下降。

所以,是为了更好的网页响应速度和性能,综合考虑设计了堆的大小限制。

垃圾回收算法

垃圾回收算法中依据对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同分代的内存施以更高效的算法。

V8的内存分代

在V8中,主要分为新生代和老生代。堆内存的空间是新生代和老生代的总和。

新生代中的对象是存活时间较短的对象。默认大小在64位系统下为32MB,在32位系统为16MB。

老生代中的对象是存活时间较长的对象。默认大小在64位系统下为1400MB,在32位系统为700MB。

V8分代.png

1.scavenge算法

新生代中的对象主要通过scavenge算法进行垃圾回收。它将内存一分为二,一个在空闲中,一个在使用中。

在使用状态的空间称为From空间,处于闲置状态的空间称为To空间。当我们分配对象时,先是在Form空间进行分配,在进行垃圾回收机制时,会检查Form空间的存活对象,这些存活对象会被复制到To空间,而非存活的对象占用的空间将会被释放。

完成把存活对象从Form空间复制到To空间后,Form空间和To空间的角色将发生对换。

scavenge算法的缺点是只能使用堆内存的一半,优先是使用空间换时间。

V8分代.png

在经过两次垃圾回收机制后依旧存活的对象,会被认为是生命周期比较长的对象,会被移动到老生代中,这个叫做对象晋升机制。

对象晋升的另一种情况是,当从Form空间复制一个对象到To空间的时候,如果To空间已经使用了超过25%,那么这个对象直接晋升到老生代空间中。

设置25%的限制值的原因是当这次scavenge回收完成后,这个To空间将变成From空间,接下来的内存分配将在这个空间继续,如果占比过高,会影响后续的内存分配。

2. Mark-Sweep 和 Mark-Compact

对于老生代中的对象,由于存活对象占较大比重,再使用Scavenge的方式会有两个问题:一是存活对象较多,复制对象的效率将会很低;另一个问题是会浪费一半的空间。为此,V8在老生代中主要采用了Mark-Sweep 和 Mark-Compact相结合的方式进行垃圾回收。

Mark-Sweep 是标记清除的意思。

先标记还存活的对象,随后清除没有被标记的对象。

Mark-Sweep的最大问题是在进行一次标记清除回收后,内存空间会出现不连续的状态。在需要分配一个大对象的时候,可能所有的碎片空间都无法满足分配,就会提前触发垃圾回收。

Mark-Sweep.png

为了解决Mark-Sweep的内存碎片问题,Mark-Compact被提出来。Mark-Compact是标记整理的意思,在Mark-Sweep基础上演变而来。它们的差别在于对象在标记死亡后,在整理的过程中,将存活的对象往一端移动,移动完成后,直接清理边际外的内存。

如下图,白色格子为存活对象

Mark-Compact.png

垃圾回收算法的对比如下图:

回收算法速度空间开销是否移动对象
Scavenge最快双倍空间(无碎片)
Mark-Sweep中等少(有碎片)
Mark-Compact最慢少 (无碎片)

由于Mark-Compact需要移动对象,所以它的执行速度不会很快,所以在取舍上V8主要使用Mark-Sweep, 在空间不足以对从新生代晋升过来的对象进行分配时才使用Mark-Compact。

刷力扣的时候正好看到v8引擎Mark-Compact报错,如下:

image.png

全停顿与增量标记

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

因为新生代的空间配置的小,存活对象也比较少,即使全停顿对JS应用的影响也不大,但是老生代空间大,存活对象较多,如果要全停顿进行回收,影响就比较大,需要设法改善。

为了降低全停顿带来的影响,V8从标记阶段入手,将原本要一口气停顿完成的动作改成增量标记(incremental marking)。

增量标记.png

使用增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样就不会让用户因为垃圾回收任务而感受到页面的卡顿了。

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

资料参考来源书籍《深入浅出node.js》