JavaScript 是一门自动垃圾回收(Garbage Collection,GC)的语言。开发者无需手动分配和释放内存,但这并不意味着内存管理可以完全被忽视。理解 V8 引擎的垃圾回收机制,不仅能帮助你写出性能更好的代码,还能在排查内存泄漏等问题时做到心中有数。
为什么需要垃圾回收
在 JavaScript 中,每当你创建一个对象、数组、函数或闭包时,引擎都会在内存中为其分配空间。如果这些内存永远不被释放,应用迟早会因为内存耗尽而崩溃。垃圾回收的核心任务就是:找出那些不再被使用的内存,并将其回收,以便后续复用。
这里的关键问题是:如何判断一块内存“不再被使用”?V8 采用的策略是从一组根对象(Root)出发,沿着引用链遍历,凡是能够到达的对象都被视为“存活”,反之则可以被回收。这就是所谓的可达性分析。
V8 的内存分代模型
V8 将堆内存分为两个主要区域:新生代(Young Generation)和老生代(Old Generation)。这种分代设计的依据是“弱分代假说”——大多数对象的生命周期都很短,只有少数对象会长期存活。
- 新生代:空间较小(通常为 1~8 MB),存放 newly 创建的对象。
- 老生代:空间较大,存放经过多次回收仍然存活的对象。
针对这两个区域,V8 采用了不同的回收算法。
新生代回收:Scavenge 算法
新生代使用 Scavenge 算法,它将新生代空间一分为二:From 空间和 To 空间。
回收过程如下:
- 新对象首先分配到 From 空间。
- 当 From 空间快满时,触发 GC。
- 引擎检查 From 空间中的对象,将存活对象复制到 To 空间,同时按顺序紧凑排列。
- 清空 From 空间,然后交换 From 和 To 的角色。
这种复制方式的优点是速度快,且天然解决了内存碎片问题。缺点是需要浪费一半的空间,但由于新生代本身很小,这个代价可以接受。
此外,Scavenge 还使用了晋升机制:如果一个对象已经经历过一次 Scavenge 仍然存活,或者 To 空间的使用率超过一定阈值(如 25%),它就会被移动到老生代。
老生代回收:标记-清除与标记-压缩
老生代中的对象存活率高,如果继续使用复制算法,效率会非常低。因此 V8 采用了 标记-清除(Mark-Sweep) 和 标记-压缩(Mark-Compact) 两种算法。
标记-清除
- 标记阶段:从根对象出发,递归遍历所有可达对象,并打上标记。
- 清除阶段:遍历整个堆,回收未被标记的对象。
标记-清除的缺点是会产生大量内存碎片。当需要分配一个较大的连续对象时,即使总空闲内存足够,也可能因为碎片而分配失败。
标记-压缩
为了解决碎片问题,V8 在标记-清除的基础上引入了标记-压缩。它在清除之前,将所有存活对象移动到内存的一端,然后直接清理边界之外的空间。这样既回收了内存,又保证了剩余空间的连续性。
V8 会根据碎片化程度决定是否执行压缩:碎片较多时启用标记-压缩,否则只做标记-清除,以减少移动对象带来的开销。
增量标记与并发回收
早期的 V8 在执行标记阶段时会暂停 JavaScript 主线程,这被称为“全停顿”(Stop-The-World)。如果老生代对象很多,停顿时间可能达到几百毫秒,导致页面卡顿。
为了减少停顿,V8 引入了以下优化:
- 增量标记(Incremental Marking):将标记过程拆分成多个小步骤,穿插在 JavaScript 执行之间,让主线程不至于长时间阻塞。
- 并发标记(Concurrent Marking):将标记工作交给辅助线程,与主线程并行执行。
- 并发清除与并发压缩:清除和压缩阶段也尽可能放到后台线程完成。
这些优化使得现代 V8 的 GC 停顿时间大幅缩短,用户几乎感知不到。
常见的内存泄漏场景
即使有自动垃圾回收,以下情况仍可能导致内存泄漏:
- 意外的全局变量:未声明的变量会挂载到
window上,无法被回收。 - 未清理的定时器:
setInterval持续引用回调中的变量,导致其无法释放。 - 闭包引用:闭包会保留对外部变量的引用,若闭包本身长期存活,外部变量也无法回收。
- DOM 引用:已从文档中移除的 DOM 节点,如果仍被 JavaScript 变量引用,也不会被回收。
- 事件监听器未移除:尤其是绑定在全局对象或长期存在的元素上的监听器。
实践建议
- 及时将不再使用的变量置为
null,尤其是大对象。 - 使用
WeakMap和WeakSet存储弱引用,避免阻碍回收。 - 在单页应用中,组件卸载时清理定时器、事件监听器和订阅。
- 利用 Chrome DevTools 的 Memory 面板进行堆快照对比,定位内存泄漏。
总结
V8 的垃圾回收机制是一套精密的分代回收系统:新生代用 Scavenge 快速回收短命对象,老生代用标记-清除和标记-压缩处理长命对象,再通过增量标记和并发回收降低停顿。理解这套机制,有助于我们写出更高效、更稳定的 JavaScript 应用。
未经允许不得转载:任鹏个人博客 » JavaScript 垃圾回收机制详解:V8 引擎如何管理内存

