V8的垃圾回收机制和内存限制
内存限制
在一般的后端开发语言,在基本的内存使用上是没有限制的。
主要是因为 Node 基于 V8构建,所以在 Node 中使用的 JavaScript 对象基本是通过 V8 自己的方式来分配和管理的。
V8 的内存管理机制在浏览器上使用是完全足够的,但是对服务端来讲,限制了开发者随心所欲使用内存的想法。
尽管我们很少的场景会使用到很大的内存,比如大文件的读取我们都可以通过 Stream 来完成,但 Node 还是提供了方法打开这个限制。
默认内存大小
- 64位系统下,约为 1.4 GB
- 32位系统下,约为 0.7 GB
查询内存信息
1 | import { memoryUsage } from 'process' |
heapTotal和heapUsed指的是 V8 的内存使用量。external指的是绑定到 V8 管理的 JavaScript 对象的 C++ 对象的内存使用量。rss,常驻集大小,是进程在主内存设备(即总分配内存的子集)中占用的空间量,包括所有 C++ 和 JavaScript 对象和代码。arrayBuffers是指为ArrayBuffer和SharedArrayBuffer分配的内存,包括所有 Node.jsBuffer。 这也包含在external值中。 当 Node.js 被用作嵌入式库时,此值可能为0,因为在这种情况下可能不会跟踪ArrayBuffer的分配。
修改内存限制
1 | node --max-old-space-size=1700 test.js // 单位是MB |
修改新生代空间的参数好像改成了:
1 | node --max-semi-space-size=1025 test.js |
可以使用:node --v8-options | greap max 来查看
垃圾回收机制
V8 使用了内存分代的方式来管理内存。
新生代中的对象为存活时间较短的对象;而老生代中的对象为存活时间较长或常驻内存的对象。

新生代空间
新生代空间主要使用 Scavenge 算法进行垃圾回收。在 Scavenge 的实现上 主要使用 Cheney 算法。
Scavenge 是一种复制算法,新生代空间会被一分为二划分成两个相等大小的 from-space 和 to-space。
它的工作方式是将 from space 中存活的对象复制出来,然后移动它们到 to space 中或者被提升到老生代空间中,对于 from space 中没有存活的对象将会被释放。完成这些复制后在将 from space 和 to space 进行互换。
缺点:
- 只能使用堆内存的一半
优点:
- 由于新生代空间,大部分都是存活时间短的对象,所以只复制存活对象,效率比较高。
晋升
对象从新生代空间移动到老生代空间的过程称为晋升。
主要晋升条件有两个:
- 对象是否经历过 Scavenge 回收
- To 空间的内存占用比是否超过 25%
老生代空间
老生代空间的对象,由于存活时间比较长,存活对象占较大比重。
所以无法采用Scavenge算法:
- 存活对象多导致复制效率低
- 浪费一半内存,对于存活对象较多的老生代空间是无法接受的
所以在老生代空间使用 Mark-Sweep 和 Mark-compact 相结合的方式进行垃圾回收
Mark-Sweep
标记清除算法分为标记和清除两个阶段。
Mark-Sweep在标记阶段遍历堆中所有对象,并标记或者的对象,在随后的清除阶段,只清除没有标记的对象。
该算法最大的问题是,在清除后,内存空间会出现不连续的状态,如果后续有一个大对象,那么这些碎片空间都无法完成这次分配,导致提前触发垃圾回收。
Mark-compact
和Mark-Sweep的差别是,Mark-compact标记完成后,在整理过程中会将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。
STW
stop-the-world,垃圾回收的三种基本算法,都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为全停顿(stop-the-world)
V8通过引入增量标记,来减少全停顿的时间
垃圾回收机制性能优化
增量标记
Incremental marking
为了降低全堆垃圾回收的停顿时间,增量标记将原本的标记全堆对象拆分为一个一个任务,让其穿插在JavaScript应用逻辑之间执行,它允许堆的标记时的5~10ms的停顿。
增量标记在堆的大小达到一定的阈值时启用,启用之后每当一定量的内存分配后,脚本的执行就会停顿并进行一次增量标记。
懒性清理
Lazy sweeping
增量标记只是对活动对象和非活动对象进行标记,惰性清理用来真正的清理释放内存。
当增量标记完成后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,可以将清理的过程延迟一下,让JavaScript逻辑代码先执行,也无需一次性清理完所有非活动对象内存,垃圾回收器会按需逐一进行清理,直到所有的页都清理完毕。
增量标记与惰性清理的出现,使得主线程的最大停顿时间减少了80%
缺点:
- 并没有减少主线程的总暂停的时间,甚至会略微增加
- 由于写屏障(Write-barrier)机制的成本,增量标记可能会降低应用程序的吞吐量
并发
Concurrent
并发式GC允许在在垃圾回收的同时不需要将主线程挂起,两者可以同时进行,只有在个别时候需要短暂停下来让垃圾回收器做一些特殊的操作。
但是这种方式也要面对增量回收的问题,就是在垃圾回收过程中,由于JavaScript代码在执行,堆中的对象的引用关系随时可能会变化,所以也要进行写屏障操作。
并行
Parallel
并行式GC允许主线程和辅助线程同时执行同样的GC工作,这样可以让辅助线程来分担主线程的GC工作,使得垃圾回收所耗费的时间等于总时间除以参与的线程数量(加上一些同步开销)。
