关于 V8引擎 的垃圾回收机制 !!!

247 阅读5分钟
开篇

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

内存分代

V8的垃圾回收策略只要基于分代式垃圾回收机制(新生代&老生代), V8堆的整体大小就是新生代所用内存空间加上老生代的内存空间

新生代

新生代中的对象为存活时间较短的对象

老生代

老生代中的对象为存活时间较长或常驻内存的对象

垃圾回收机制

Scavenge 算法(新生代)

定义

在 Scavenge的具体实现中,主要采取了Cheney算法;Cheney算法是一种采用复制的方式实现的垃圾回收算法.将堆内存一分为二,每一部分空间称为semispace. 在这两个semispace空间中,只有一个处于使用中,里一个处于闲置状态.处于使用状态的semispace空间称为From空间, 处于闲置状态的空间称为To 空间.

当开始垃圾回收时,先检查From空间中的存活对象,这些存活对象将复制到To 空间中,非存活对象占用的空间将会被释放. 完成复制后,From空间和To空间的角色发生对换, 总的就是 ,在垃圾回收的过程中,就是通过将存活对象在俩个semispace空间进行复制.

由于只能复制存活对象,并且对于生命周期短的场景存活对象只占少部分,所以在时间效率上有优异的表现.

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

对象晋升的条件主要有两个,一个事对象是否经历过Scavenge回收,一个是To空间的内存占用比超过限制.

是否经历过Scavenge回收

To 内存超过限制

缺点

Scavenge 只能使用对内存的一半,是由划分空间和复制机制所决定的. 并且是单典型的空间换取时间的算法,所有无法大规模地应用到所有的垃圾回收中

Mark-Sweep(标记清除)

分为标记和清除两个阶段,与Scavenge相比,不存在浪费一半空间的行为,Mark-Sweep在标记阶段遍历堆中所有的对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象.

下图黑色部分标记为死亡的对象

缺点

Mark-Sweep在进行一次标记清除回收后,内存空间会出现不连续的状态.这种内存碎片化会对后续的内存分配造成问题.很可能出现需要分配一个大对象的情况,这是所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收.

Mark-Compact(标记处理)

主要解决 Mark-Sweep碎片化问题,在 Mark-Sweep的基础上演变而来, 区别在于对象在标记死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界的内存

下图白色个格子为存活的对象,深色格子为死亡对象,浅色格子为存活对象移动后立项的空洞

由于Mark-Compact需要移动对象,所有它的执行速度不可能很快,所以V8只要使用Mark-Sweep,在空间不足以对从新生代中晋升过来的对象进行分配是才时才用Mark-Compact

Incremental Marking(增量标记)

为了避免出现JavaScript应用逻辑与垃圾回收器看到的不一致情况,垃圾回收的3中方式基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再回复执行应用逻辑,这种行为被称为"全停顿(stop-the-world)". V8老生代中通常配置得较大,且存活对象较多,全堆垃圾回收(full垃圾回收)的标记,清理,整理等动作造成的停顿就会很可怕.

为了降低全堆垃圾回收带来的停顿时间,将原来要一口气要完成停顿的动作改为增量标记(Incremental Marking),就是拆分为许多小"步进",没做完一"步进"就让JavaScript应用逻辑执行一小会,垃圾回收与应用逻辑交替执行知道标记完成

改进后停顿时间减少到原来的 1/6

导致内存泄漏 的原因

  • 缓存
  • 队列消费不及时
  • 作用域未释放

内存当缓存

在node中一旦一个对象被当做缓存来使用,那就意味着它将来会常驻在老生代中,缓存中存储的键越多,长期存活的对象也就越多,将导致垃圾回收在进行扫描整理时 无用功.

模块也有缓存机制,且模块是常驻老生代 (Node模块);

进程之间无法共享缓存, 解决方案是采用进程外的缓存,进程自己不存储状态.外部的缓存软件有着良好的缓存过期淘汰策略以及自由的内存管理

队列

队列在消费者-生产者中经常充当中间产物,这是容易忽略的情况,大多数应用场景下,消费速度远远大于生产速度,内存泄漏不易产生,一旦消费速度低于生产速度,将会形成堆积,

表层的解决方案是换用消费速度更高的技术,深度的解决方案应该是监控队列的长度,一旦堆积,应当通过监控系统产生报警并通知相关人员.另一个解决方案是任意异步调用都应该包含超时限制,一旦在限定的时间内完成响应,通过回调函数传递超时异常,使得任意异步调用的回调都具有响应时间

参考

深入浅出node.js

结语

前端界的小学生!!!