V8引擎垃圾回收机制

       js与java一样,由垃圾回收机制来进行自动内存管理,这使得开发者不需要像C/C++程序员那样在编写代码的过程中时刻关注内存的分配和释放问题。

V8的内存限制

       在一般的后端开发语言中,在基本的内存使用上没有什么限制,然而在Node中通过js使用内存时就会发现只能使用部分内存(64位系统下约为1.4 GB,32位系统下约为0.7GB)。在这样的限制下,将会导致Node无法直接操作大内存对象,比如无法将一个2GB的文件读入内存中进行字符串分析处理。

     造成这个问题的主要原因在于Node基于V8构建,所以在Node中使用的js对象基本上都是通过V8自己的方式来进行分配和管理的。

V8的对象分配

    在V8中,所有的js对象都是通过堆来进行分配的。Node提供了V8中内存使用量的查看方式:

其中,heapTotal和heapUsed是V8的堆内存使用情况,heapTotal是已申请到的堆内存, heapUsed是当前使用的量。

下图为V8的堆示意图:

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

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

    当然,V8依然提供了选项来让我们使用更多的内存,Node在启动时可以传递--max-old-space-size或--max-new-space-size来调整内存限制的大小

node --max-old-space-size=1700 xx.js // 单位为MB,调整老生代这部分的内存
// 或者
node --max-new-space-size=1024 xx.js // 单位为kB,调整新生代这部分的内存

V8的垃圾回收机制

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

下图为V8的分代示意图:

    V8堆的整体大小就是新生代所用内存空间加上老生代的内存空间。--max-old-space-size命令行参数可以用于设置老生代内存空间的最大值, --max-new-space-size命令行参数则用于设置新生代内存空间的大小的。

    对于新生代内存,内存最大值在64位系统和32位系统上分别为32MB和16MB。新生代中的对象主要通过Scavenge算法进行垃圾回收。主要采用了Cheney算法,这是一个采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,每一部分称为semispace,在这两个空间中,只有一个处于使用中(称为From空间),另一个处于闲置状态(称为To空间)。当我们分配对象时,先是在From空间中进行分配,在开始进行垃圾回收时,会检查From空间中的存活对象,这些存活对象将被复制到To空间中,而非存活对象占用的空间将会被释放。完成复制后,From空间和To空间的角色发生兑换。

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

    Scavenge是典型的牺牲空间换时间的算法,所以非常适合应用在新生代中,因为新生代中对象的生命周期较短。

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

    在单纯的Scavenge过程中,From空间中的存活对象会被复҃制到To空间中去,然后对From空间和To空间进行角色对换(又称翻转)。但在分代式垃圾回收的前提下,From空间中的存活对象在在复҃到To空间之前需要进行检查。在一定条件下,需要将存活周期长的对象移动到老生代中, 也就是完成对象晋升。

    对象晋升情况:

  • 对象是否经历过Scavenge回收;
  • To空间的内存占用比超过25% 

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

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

    Mark-Sweep是标记清除的意思,它分为标记和清除两个阶段。在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除对象中,只清除没有被标记的对象。可以看出,Scavenge中只复制活着的对象,而Mark-Sweep只清理死亡对象。

如图:黑色部分标记为死亡的对象。

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

完成移动后,就可以直接清除最右边的存活对象后面的内存区۪域完成回收。

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

增量标记(Incremental Marking)

    由于JS的单线程机制,V8在执行垃圾回收时都会将应用逻辑暂停下来,如果V8老生代配置较大,且存活对象较多,全堆垃圾回收(full垃圾回收)的标记、清理、整理等动作造成的影响就会很大,为了降低回收垃圾回收带来的停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记(incremental marking),也就是拆分为许多小的部分,每做完一部分就让JavaScript应用逻辑执行一小会, 垃圾回收与应用逻辑交替执行直到标记阶段完成。

如下图所示:

    V8在经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原本的1/6左右。

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

 

参考资料:

深入浅出Node.js

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值