Posts 浅谈Node.js的垃圾回收机制
Post
Cancel

浅谈Node.js的垃圾回收机制

  总结下关于 Node 的 GC 的理解.众所周知, Node 是构建在 V8 引擎之上, Node 和 Chrome 架构如图1-1所示 (webKit: 布局引擎, libuv: 多平台兼容组件).而 V8 引擎可以说是 JS 的一个虚拟机.就像 JAVA 虚拟机一样,也是通过垃圾回收机制来进行内存管理的.

图1-1 Chrome 和 Node 的组件

  在 V8 中,所有的 JavaScript 对象都是通过堆来进行存分配的,而 V8 对堆内存大小有一个限制,64位系统下最大为1.4G(新生代32M,老生代1400M),32位系统下最大为0.7G(新生代16M,老生代700M).做这个限制的原因是因为,如我们的需要进行回收的堆内存有 1.5G ,V8 做一次小的 GC 要 50ms 以上,做一次非增量的 GC 要 1s 以上,这就是意味着你的系统会卡顿 1s.让系统停止运行 1s 是不能够被接受的,如果不做限制可想而知.不过这个限制也是能放开的. Node 在启动的时候可以传递 --max-old-space-size--max-new-space-size 这两个环境变量来修改,可以看到这两个变量命翻译过来的意思是新生代最大内存和老生代最大内存.而 V8 的堆内存的也是分为这两代,如图1-2所示.这两个分代采用的 GC 策略也是不同的.

1
2
node --max-old-space-size=1024 index.js // 单位为MB
node --max-new-space-size=1024 index.js // 单位为KB
图1-2 新老生代在V8堆内存中的分布

  为什么在 GC 的时候会需要停止代码的运行呢?那是因为,为了避免代码逻辑和垃圾回收器看到不一致的情况,就像数据库中的这个情况,在写一行数据的时候会把这行数据锁住,防止程序读到脏数据.所以永远都需要等到 GC 运行完毕后才能继续执行代码逻辑,这个行为称为’全停顿’在V8的分代回收策略中,新生代由于对象数量不多,GC 执行一次听课的,所以影响不大.但是在老生代中一次 GC 的时候就需要好久,所以 V8 又引入了增量 GC 这个概念,就是把一次执行完毕的 GC 分为了多次执行.这就有点像操作系统的多进程的感觉了,宏观上并行,微观上串行,一会执行A进程一会执行B进程,给人的感觉就是两个进行都在运行,而实际在底层只要一个进程在运行.这样分多次 GC 给人的感觉就不卡顿了.

1.新生代的垃圾回收机制

  新生代的内存空间只会放置存活时间较短的对象, 新生代的GC策略采用的是 Scavenge 算法, 它会把新生代的内存空间一分为二,这两块空间一个处于闲置状态而一个处于工作状态.给新对象分配空间的时候都会分配到工作空间中去,当要进行 GC 的时候,会把存活的对象都复制到闲置状态的空间里,而非存活的对象都将被释放掉.然后,闲置状态就变成工作状态,原来的工作状态就变成了闲置状态,这样的效率很高,但是却牺牲了一半的内存空间.在经过多次新生代的GC还存活的对象将被丢进老生代的内存空间中,由老生代的 GC 策略管理,这个过程被称为晋升.

  对象晋升到老生代的条件有两个, 一个是检查内存地址来判断这个对象是否经过了新生代的 GC ,一个是判断闲置空间的内存使用率是否超过25%.为什么要超过25%就要被晋升呢?因为闲置空间和工作空间需要互换角色,如果闲置空间过满,在变成工作空间后会导致接下来的新对象没空间可用.

2.老生代的垃圾回收机制

  老生代采用的的GC策略和新生代是不同的,因为老生代的对象都是经过多次 GC 还存活的对象,存活的对象数量很多,如果和新生代使用相同的策略的话,那么复制对象就需要好久的时间.而且还有一点关键的就是内存使用率只有50%.所以老生代带用的策略是 Mark-Sweep(标记清除) 和 Mark-Compact(标记整理),标记清除就是遍历老生代空间中的全部对象, 给活着的对象打上标记, GC 执行的时候把没有标记的对象清理掉就行了.怎么识别那些对象还活着呢?我这篇文章JS的垃圾回收简单的讲了下怎么识别对象的死活.

  但是 Mark-Sweep 有一个问题就是他没有移动对象,就会导致清理死亡对象后有很多内存碎片.如图2-1所示,黑色部分就是死亡对象.这样导致老生代空间会有大量不连续的内存空间.那内存碎片多了有什么影响呢? 内存碎片多了的话会导致我们没办法分配大的内存对象,比如数组.我们都知道数组在内存中是需要一片连续的空间.还是如图2-1,在这样的情况下我们就没办法分配 1/2 老生代内存大小的数组了.

图2-1 Mark-Sweep 标记后内存分布

  为了解决内存碎片的问题,于是基于 Mark-Sweep 提出了 Mark-Compact, Mark-Compact 相较于 Mark-Sweep 的区别是会在标记的时候把死对象往一边移动,移动完毕后清理一边的死对象就行了.如图2-2所示,白色为存活的对象,黑色为死对象,灰黑色为存活对象移动留下的空间.这样清理完毕后,灰黑色部分就是一个连续的内存空间了.

图2-2 Mark-Compact 标记并移动存活对象后内存分布

  在老生代中 Mark-Sweep 和 Mark-Compact 是一起使用的,但是因为 Mark-Compact 需要移动对象,所以效率肯定没有 Mark-Sweep 好, 经过舍取 V8 主要还是采用效率高一点的 Mark-Sweep 算法,只有空间不足,比如出现像我上文提到需要分配大对象这样的极端情况的时候才会采用 Mark-Compact 算法.图2-3是三种 GC 算法的比较

图2-3 三种 GC 算法的比较

3.堆外内存 & 大内存应用

  我们通过上文了解到在默认情况下最大内存只能使用1.4G的内存空间.如果我们需要用到更大的内存空间怎么办呢? 可以使用 Buffer 对象, Buffer 构建的对象不是通过V8 来进行分配的,这个就被称为堆外内存. Buffer 是 Node 中的 C++ 模块找操作系统要的,这一块的 GC 在这里就不讨论了.

  日常中难免需要读取一个大文件啥的. 我们可以使用 Node 提供的 stream 模块,由于 V8 的内存限制,没办法使用 fs.readFile() 和 fs.writeFile()来操作大文件, 只能通过 fs.createReadStream() 和 fs.createWriteStream() 通过文件流的形式对文件进行操作.

4.Ref

  1. «深入浅出Node.js» 第五章
This post is licensed under CC BY 4.0 by the author.

Azure Automation Account Runbook 入门

从0到1构建自己具有https协议的服务器