V8垃圾回收机制

@fangwentian 2018-03-23 06:26:09发表于 kaola-fed/blog 待归档

本文是深入浅出nodejs的部分学习笔记

V8内存限制

在node中javascript能使用的内存是有限制的,64位系统下约为1.4GB,32位系统下约为0.7GB

这个限制在node启动的时候可以通过传递--max-old-space-size 和 --max-new-space-size来调整,如:

node --max-old-space-size=1700 app.js //单位为MB
node --max-new-space-size=1024 app.js //单位为MB

上述参数在V8初始化时生效,一旦生效就不能再动态改变。

V8垃圾回收机制

V8的内存分代

在V8中主要将内存分为新生代和老生代。新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。

V8堆的整体大小就是新生代所用内存加上老生代所有内存。前面提到的--max-old-space-size用来设置老生代内存空间的最大值,--max-new-space-size用来设置新生代内存空间的最大值。

Scavenge算法

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

Cheney算法是一种采用复制的方式实现的垃圾回收算法,它将堆内存一分为二,这两个空间中只有一个处于使用中,一个处于闲置状态。处于是使用状态的空间称为From空间,处于闲置的空间称为To空间。分配对象时,先是在From空间中进行分配,当开始垃圾回收时,会检查From空间中的存活对象,并将这些存活对象复制到To空间中,而非存活对象占用的空间被释放。完成复制后,From空间和To空间的角色互换。简而言之,垃圾回收过程中,就是通过将存活对象在两个空间中进行复制。

Scavenge算法的缺点是只能使用堆内存中的一半,但由于它只复制存活的对象,对于生命周期短的场景存活对象只占少部分,所以在时间效率上有着优异的表现。

以上所说的是在纯Scavenge算法中,但是在分代式垃圾回收的前提下,From空间中存活的对象在复制到To空间之前需要进行检查,在一定条件下,需要将存活周期较长的对象移动到老生代中,这个过程称为对象晋升。

对象晋升的条件有两个,一种是对象是否经历过Scacenge回收:

令一种情况是当To空间的使用应超过25%时,则这个对象直接晋升到老生代空间中。

Mark-Sweep 和 Mark-Compact

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

Mark-Sweep是标记清除的意思,它分为标记和清楚两个阶段。与Scavenge相比,Mark-Sweep并不将内存空间划分为两半,所以不存在浪费一半空间的行为。与Scavenge复制活着的对象不同,Mark-Sweep在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清理阶段中,只清除没有被标记的对象。可以看出,Scavenge只复制活着的对象,而Mark-Sweep只清理死亡对象。活对象在新生代中只占较小部分,死对象在老生代中只占较小部分,这就是两种回收方式能高效处理的原因。

Mark-Sweep在老生代空间中标记后的示意图:

Mark-Sweep最大的问题在于进行一次标记清楚后,内存会出现不连续的状态,这种会导致后续需要分配一个大对象的时候,无法完成分配,就会提前出发垃圾回收,而这次回收是不必要的。

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

3种回收算法的简单比较:

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