浅谈V8的垃圾回收机制

avatar
前端

node想玩的溜溜的,垃圾回收机制必须的了解。在服务端,资源是很宝贵的东西。写合理的代码,可以让程序的质感都不一样。

V8的垃圾回收机制

  • 分代以及内存限制大小

    • V8的垃圾回收策略主要基于分代式垃圾回收机制。在实际应用中对象的生存周期不一样,不同的算法只能针对特定的情况才具有最好的效果。为此现代的垃圾回收算法按对象的存活时间将内存的垃圾进行不同的分代,然后对不同分代的内存用更高效的算法。
    • V8的内存分代
      • 新生代:中的对象存活时间较短
      • 老生代 中的对象存活时间较长(或常驻内存的对象)
      • 对于新生代:它由两个reserved_semispace_size_所构成,机器按位数的不同分为(64位:16MB ,32位:8MB),则新生代内存的最大值为64位:32MB,32位:16MB
    • V8堆内存默认情况下最大值:64位:1464MB,32位:732MB;这就可以解释为何64位下只能使用1.4G, 32位下 .7G。
    • 这个内存的限制,在V8的源码里可以看到。
  • 垃圾回收算法

    • Scavenge算法:
      • 在分代的基础上,新生代主要用的算法是Scavenge算法进行垃圾回收,Scavenge实现又是用Cheney算法
      • Cheney采用一种赋值的方式实现垃圾回收,将堆内存一分为二,称其semispace,其中一个空闲,一个在使用,处于使用状态的semispace称为From空间,另一个称为To空间。
      • 当我们分配对象内存时,从From空间开始分配。当开始垃圾回收时,检查From空间存活的对象,这些对象将被复制到To空间,非存活的将被释放。然后将From和To空间的角色对换,接着下次的回收做准备。
      • Scavenge缺点就是只能使用堆内存的一半,由于只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,时间效率就高
      • Scavenge是典型的牺牲空间换时间,不能大规模的使用在所有的垃圾回收中,其适用于新生代,在新生代中,对象的生命周期短。
    • 当一个对象经过一次Scavenge算法后(多次复制),就会被认为是生命周期较长的对象,则其会被迁移到老生代中,用新的算法进行管理,从新生代到老生代的过程叫晋升。
    • 对象晋升的条件主要有两个:
      • 一: 对象是否经历过Scavenge算法回收
      • 二:To空间的内存占用比超过限制(25%),规定这个值是因为:当Scavenge算法回收后,To空间将变成From空间,接下来内存的分配将在这里进行,若占用过高会影响接下来的分配。
    • Mark-Sweep(标记清除) & Mark-Compact(标记整理)
      • 在老生代中:Scavenge就显得很无力,老生代存活对象占比大,复制效率会很低;还有就是浪费一半空间的问题。
      • Mark-Sweep(标记清除):分为标记和清除两个阶段。Mark-Sweep在标记阶段遍历堆中的所有对象,并标记活着的对象。在清理阶段,清理没有标记的对象。
      • Scavenge只复制活着的对象,Mark-Sweep只清理死亡的对象。活对象在新生代中只占较小部分,死对象在老生代中只占较小部分
      • **标记清除的问题:**在进行一次标记清除后,内存空间会出现不连续的状态。这种内存碎片会对后续的分配造成问题,如分配一个大对象,所有内存碎片都无法完成这次分配,就会提前触发垃圾回收,而这次回收是没有必要的。
      • Mark-Compact(标记整理): 这个是为了弥补Mark-Sweep内存碎片问题的。其由Mark-Sweep演变而来,区别在于对象标记为死亡时,在整理中将活的对象往一边移动,死的对象往另一边移动,移动完后,直接清除边界外的内存。(执行速度不是很快,因为要移动对象,V8在取舍上,主要使用Mark-Sweep,老生代空间不足以对从新生代晋升的对象进行分配时才使用Mark-Compact)
    • 增量标记(Incremental Marking)
      • 为了避免出现JS应用逻辑和垃圾回收器看到的不一样,垃圾回收的3种基本算法都需要将应用逻辑暂停下来,执行完垃圾回收后才接着执行应用逻辑(称‘全停顿’)。在老生代中,对象多,全停顿就会比较可怕,影响应用逻辑长时间停顿。
      • 为了降低去堆栈回收带来的停顿时间,:
        • 标记阶段入手:将一口气停顿完成的标记,变成增量标记,也就是拆分为几个小“步进”,每做完一次小步进,就让JS应用逻辑执行一会,交替执行到标记完成。这样时间可以减少到原来的1/6。
        • V8后续还引入:延迟清理,增量式整理,让清理和整理也变成增量式。同时还计划引入:并行标记与并行清理,这是进一步利用多核性能降低每次停顿的时间。
    • 小结:从垃圾回收机制可以看到,V8对内存限制的缘由。
      • 对于node写服务端,内存限制并不影响正常使用,但是对于V8的垃圾回收机制和JS在单线程上执行,垃圾回收是影响性能的因素之一。想要高性能的执行效率,应减少垃圾回收触发的频率,尤其是全堆的垃圾回收。

查看垃圾回收的日志

  • 启动时加上:--trace_gc参数,
node --trace_gc -e "var a = [];for (var i = 0; i < 1000000; i++) a.push(new Array(100));" > gc.log

回收日志(只截取了一小部分):

  • 看V8执行时的性能分析数据:启动时: 使用--prof参数
$ node --prof test01.js
//这会在目录下得到一个v8.log日志---》可读性差
  • V8提供linux-tick-processor工具用于统计日志信息,可以在node 源码的deps/v8/tools目录下找到。windows下的对应命令文件为windows-tick-processor.bat.