V8的垃圾回收机制和内存限制
V8 的内存限制
在一般的后端开发语言中都是没有内存限制的,由于基于 v8 最初是为浏览器设计的,64 位下的 1.4GB 和 32 位下的 0.7GB 的限制值用起来是绰绰有余的,导致 Nodejs 在开发的过程中只能在该限制下运行,这将会导致 Nodejs 无法直接操作大内存对象,比如无法将一个 2GB 的文件读入内存中进行字符串分析处理,即使物理内存有 32 个 G。
V8 内存限制的原因
- 表面原因是浏览器对 1.4GB 和 0.7GB 的使用绰绰有余
- 深层原因是 v8 的垃圾回收机制的限制。按照官方说法,以 1.5GB 的垃圾回收堆内存为例,v8 做一次小的垃圾回收需要 50ms 以上,做一次非增量式的垃圾回收甚至需要 1s 以上。因为 js 的执行是单线程的,在垃圾回收时会暂停 js 的执行,导致应用的性能和响应能力直线下降。(垃圾回收是在特定的时候执行的)
当然,Nodejs 在启动的时候也可以通过传递参数来调整内存限制的大小,实例如下:
node --max-old-space-size=1700 test.js // 老生代内存空间大小。单位为MB
node --max-new-space-size=1024 test.js // 新生代内存空间大小。单位为MB
V8 的垃圾回收机制
V8 的内存分代
在 v8 中内存分为新生代和老生代两代。新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。
Scavenge(清道夫)算法
新生代中的对象主要通过 Scavenge 算法进行垃圾回收,它将堆内存一分为二,每一部分空间称为 semispace。在这两个 semispace 空间中,只有一个处于使用状态,另一个处于闲置状态。处于使用状态的 simispace 空间称为 From 空间,处于闲置状态的空间称为 To 空间。当开始从 From 空间进行垃圾回收时,会将存活对象复制到 To 空间中,非存活对象的空间将会被释放。完成复制后 From 空间和 To 空间角色互换。(相当于漏斗一样,无限过滤)
Scavenge 算法的缺点是只能使用堆内存的一半,是典型的空间换时间的算法,所以无法大规模应用到所有的垃圾回收中。但可以发现,Scavenge 非常适合应用在新生代中,因为新生代中对象的生命周期较短,恰恰适合这个算法。
晋升
对象从新生代移动到老生代的过程称为晋升,晋升的条件主要有两个。
- 对象是否经历过 Scavenge 回收
2.To 空间的内存占比是否超过 25%
设置 25%这个限制原因是当这里 Scavenge 算法完成后,这个 To 空间将变成 From 空间接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。
Mark-Sweep
对于老生代中的对象,由于存活时间较长,再采用 Scavenge 的方式会有两个问题:
- 存活对象过多,复制存活对象向的效率将会很低(筛选不掉很多非存活对象,浪费时间)
- 浪费一半的内存空间
Mark-Sweep 是标记清除的意思,它分为标记和清除两个阶段。在标记阶段遍历堆中所有对象,并标记存活的对象,在随后的清除过程中只清除没有标记的对象。 可以看出,Scavenge 只复制活着的对象,Mark-Sweep 只清除死亡的对象。由于活对象在新生代中占比少,死对象在老生代中的占比少,这就是两种方式各自高效 的原因。
缺点
清除后的内存空间会出现不连续的现象,这种不连续的内存称为“内存碎片”。内存碎片会对后续的内存分配造成问题,因为有可能新生代中晋升的对象需要的内存较大,但是老生代中没有可直接分配的内存大小,对于这种需要分配一个大对象的况,这时所有的”碎片空间“都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。
Mark-Compact
Mark-Compact 是标记整理的意思。它和 Mark-Sweep 的区别是,在标记对象死亡后,在整理的过程中,将或者的对象往一端移动,移动完成后直接清理掉边界外的内存。(也就是清理以下的空洞内存)
v8 主要使用 Mark-Sweep,在空间不足以从新生代中晋升的对象分配时才使用 Mark-Compact
增量标记
在执行三种垃圾回收算法时都需要将 js 主线程的应用逻辑停顿下来,待执行完垃圾回收之后再恢复执行应用逻辑。这种行为称为“全停顿”。对于新生代默认得较小,且其中存活的对象较少,即使全停顿也影响不大。但对于 v8 对老生代的配置较大,而且存活对象较多,“全停顿”造成的后果是非常可怕的,需要设法改善。
增量标记就是拆分为许多小步前进,每做完一步,就让 js 的应用逻辑执行一小会儿。两者互相交替执行。
v8 后续要引进了“延迟清理“,”增量式整理“,”并行标记“,”并行清除“等,进一步地减少垃圾回收对主线程的影响,为应用提升更多的性能。
高效使用内存
在正常的 js 执行中,无法立即回收的内存有”闭包”和“全局变量”引用这两种情况。由于 v8 的内存限制,要十分小心此类变量是否无限制的添加,因为他会导致老生代中的对象增多。所以建议如下:
- 尽可能少的使用全局变量
- 手动清除定时器
- 少用闭包
- 清除 DOM 引用
- 弱引用,如 ES6 中的
WeakMap
和WeakSet
就是为了解决内存泄漏的问题而诞生的