JavaScript 垃圾回收机制
面试回答: JavaScript 引擎会自动管理内存。垃圾回收器从全局对象、当前调用栈等根节点出发,标记所有仍然可达的对象,再回收不可达对象。现代引擎通常还会结合分代、增量和并发回收降低停顿时间。需要注意,GC 判断的是对象是否“可达”,而不是业务上是否“还有用”;无用对象如果仍被定时器、事件监听器或全局缓存引用,依然不会被回收,这才会形成内存泄漏。
1. 内存的生命周期
JavaScript 中的内存使用可以概括为三个阶段:
- 分配:创建对象、数组、函数或闭包时,引擎为其分配内存。
- 使用:代码通过变量和属性引用这些值。
- 回收:当一个值无法再从任何有效入口访问时,引擎释放其占用的内存。
开发者通常不需要手动释放内存,但需要管理好对象之间的引用关系。
2. GC 如何判断对象能否回收?
核心判断标准是可达性(Reachability)。
垃圾回收器会从一组天然可达的“根”开始查找,例如:
- 全局对象以及全局变量;
- 当前调用栈中的局部变量和函数参数;
- 正在执行的闭包所保留的变量;
- 浏览器宿主仍持有的回调或 DOM 引用。
从根节点沿着引用关系能够访问到的对象称为可达对象,必须保留;无法从任何根节点访问到的对象称为不可达对象,可以回收。
因此,把某个变量赋值为 null 并不会“强制 GC”,它只是断开了一条引用。对象是否能被回收,取决于是否还存在其他可达路径。
3. 引用计数为什么解决不了循环引用?
引用计数的思路是记录一个对象被引用的次数,计数归零时立即回收。它容易理解,但单独使用时无法正确处理循环引用:
函数执行结束后,外部已经无法访问 left 和 right,但两个对象仍互相引用。如果只看引用次数,它们的计数都不为零;如果看可达性,它们已经无法从根节点访问,因此可以一起回收。
现代 JavaScript 引擎不会仅靠引用计数完成垃圾回收。
4. 标记清除的基本过程
标记清除(Mark-and-Sweep)是理解现代 GC 的基础模型:
- 标记:从根节点出发遍历对象图,标记所有可达对象。
- 清除:回收没有被标记的对象所占用的内存。
循环引用本身并不等于内存泄漏。只要整个循环引用结构已经与根节点断开,它仍然会被标记清除算法回收。
5. 现代引擎如何降低 GC 停顿?
如果每次回收都完整扫描整个堆,主线程可能出现明显停顿。现代引擎会在可达性分析的基础上组合多种优化策略。
(1) 分代回收
大多数新对象存活时间很短,因此引擎通常按对象存活时间划分内存区域:
- 新生代:存放新创建的对象,空间较小,回收频繁;
- 老生代:存放经历多次回收后仍然存活的对象,回收频率较低。
以 V8 为例,新生代常使用复制式回收,老生代则结合标记清除和标记整理。分代的重点不是算法名称,而是避免每次都扫描全部长期存活对象。
(2) 增量与并发回收
- 增量标记:把一次较长的标记工作拆成多个小步骤,穿插在 JavaScript 执行之间。
- 并发回收:将部分 GC 工作放到辅助线程执行,减少主线程承担的工作量。
- 空闲时回收:尽量利用主线程相对空闲的时间执行合适的回收任务。
这些策略只能降低停顿影响,无法保证 GC 完全不打断程序执行。
6. GC 为什么无法自动解决内存泄漏?
GC 只知道“还能不能访问”,不知道“业务上还需不需要”。
只要全局变量 cache 一直存在,其中的对象就始终可达。即使业务已经不再需要这些对象,垃圾回收器也不能擅自释放它们。
这也是垃圾回收与内存泄漏的边界:
- 垃圾回收解决不可达对象的自动释放;
- 内存泄漏通常来自无用对象仍被长期存活的引用持有。
具体的泄漏场景和排查方法见常见内存泄漏。
7. WeakMap 与弱引用
当对象关联数据不应该阻止对象被回收时,可以考虑 WeakMap:
WeakMap 不会因为键仍存在于映射中,就单独阻止该键对象被回收。它适合保存 DOM 元素、组件实例等对象的附加信息。
但弱引用不是通用缓存方案:其内容何时被回收不可预测,也不能依赖它完成必须执行的资源清理。定时器、事件监听器、网络连接和 Worker 仍然需要显式关闭或解绑。
8. 高频追问
循环引用一定会导致内存泄漏吗?
不一定。现代 GC 判断的是对象是否从根节点可达,而不是对象之间是否互相引用。只有循环引用结构仍被某个根节点间接持有时,它才无法被回收。
可以在 JavaScript 中主动触发 GC 吗?
标准 JavaScript 没有提供可靠的强制 GC API。开发者应该解除不必要的引用和外部资源,而不是依赖手动触发垃圾回收。
内存占用高就代表发生泄漏了吗?
不代表。引擎可能保留已申请的堆空间以便复用,短时间的大对象分配也会造成峰值。真正需要关注的是:重复执行同一操作并经过 GC 后,仍有不应存活的对象持续累积。
9. 总结
- GC 的判断核心是可达性。
- 标记清除能够处理已经与根节点断开的循环引用。
- 分代、增量和并发回收用于降低回收成本与主线程停顿。
- GC 不理解业务语义;无用对象只要仍然可达,就不会被自动释放。

