分代回收
Intro to V8
Node 的 GC 特点
输入 process.memoryUsageO)
可以查看 node 内存的使用情况:
process.memoryUsage
返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节:
- rss(resident set size):所有内存占用,包括指令区和堆栈。
- heapTotal:"堆"占用的内存,包括用到的和没用到的。
- heapUsed:用到的堆的部分。
- external: V8 引擎内部的 C++ 对象占用的内存。
Node 启动时默认的内存大小分配:
- 64bit: 1.4GB
- 32bit: 0.7GB
node 程序启动时候可以手动设置,但启动后不能动态更改:
node --max-old-space-size=1700 app.js # 单位为MB
node --max-new-space-size=1024 app.js # 单位为KB,因为新生代空间比较小
无法读取大文件到内存,带着手镣铐跳舞,因为:
- 浏览器用不到。
- GC,1.5GB 垃圾需要 1s 左右的回收的时间,会阻塞 JS 主线程。
现代的垃圾回收器改良了算法,但是本质是相同的:可达内存被标记,其余的被当作垃圾回收。特点:
- 基于分代式垃圾回收机制
- 不同场景利于利用不同 GC 算法(和统计学相关)
新生代
Scavenge 算法
Scavenge,拷贝-收集算法,它将新生代划分为两半即 semi space ,将存活的对象从 From 空间拷贝到 To 空间,过程中也会判断是否需要将对象“晋升”到老生代,拷贝完后,From 空间的对象将会被回收。然后将 To 空间“翻转”成“From”空间,下次回收的时候再进行以上流程。
这是一种典型的以空间换时间的算法,空间利用率低,其速度也最快,只适用于生命周期短的场景。在 64bit 计算机上两块最大总共占 32MB 空间,故 max-new-space-size
单位也是用 KB。
晋升:对象多次拷贝后依然存活将会晋升到老生代。
晋升条件:
- 对象是否经历过 Scavenge 回收
- To 空间的内存占比是否超过 25%(如果占比过高,To 转成 From 后会影响后续内存分配)
老生代
老生代为什么不用 Scavenge 算法?
- 老生代中的存活对象较多,复制存活对象的效率较低
- Scavenge 算法会浪费一半的存储空间
Mark-Sweep 算法
就如它的字面意思一样,Mark-Sweep 算法由标记阶段和清除阶段构成。标记阶段是把所有活动对象都做上标记的阶段。清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段。
- 标记阶段:遍历老生代堆中所有对象(这些对象是从新生代晋升过来的),并标记为存活对象。
- 清除阶段:对老生代空间中没有被标记的变量进行清除。
存活对象:存活对象可以简单理解为在执行"环境"中的那些变量和被执行环境所引用的那些变量(闭包)。
通过这两个阶段,就可以令不能利用的内存空间重新得到利用。Mark-Sweep 算法的操作过程如下图所示[4]:
图 执行 GC 前堆的状态
图 设置标志位的处理
图 标记阶段结束后的堆状态
图 清除阶段结束后的堆状态
变量清除后的问题:碎片化
对死对象进行清除后,内存可能会出现不连续的状态(如下图),逐渐产生被细化的分块,不久后就会导致无数的小分块散布在堆的各处,这种情况会对后续的内存分配造成麻烦。我们称这种状况为碎片化(Fragmentation)。众所周知,Windows 的文件系统也会产生这种现象。
目前,IE、Firefox、Opera、Chrome 和 Safari 的 JavaScript 实现使用的都是标记清除式的垃圾回收策略(或类似的策略),只不过垃圾收集的时间间隔互有不同。
在 V8 的源代码里是不存在 MarkSweep 类这种东西的。MarkSweep 算法是在 MarkCompact 类里实现的。MarkSweep 算法和 MarkCompact 算法的实现有很多相似的部分,可能想用同一个类来搞定它们[5]。
Mark-Compact 算法
V8 GC 辅助算法,当从新生代晋升过来的对象过大,空间不足时候才会使用该算法。
Mark-Compact,标记整理。它是 Mark-Sweep 算法的增强,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。