JavaScript垃圾回收采用标记清除而非引用计数,因前者能正确处理循环引用且逻辑可靠;后者因无法解决循环引用和性能开销已被弃用。

JavaScript 的垃圾回收(Garbage Collection, GC)是自动管理内存的机制,核心目标是识别并释放那些**不再被程序访问的内存**。主流引擎(如 V8)主要采用标记清除(Mark-and-Sweep),而引用计数(Reference Counting)曾被部分早期引擎尝试,但因固有缺陷已被弃用。理解两者的原理与差异,有助于写出更健壮、低内存泄漏风险的代码。
标记清除:现代 JS 引擎的实际方案
标记清除分两个阶段:先“标记”所有可达对象(从全局对象、函数调用栈等根节点出发,递归遍历引用链),再“清除”所有未被标记的对象。
-
能正确处理循环引用:比如
objA.ref = objB; objB.ref = objA;,只要 objA 和 objB 都不再被外部引用,它们会在下一次 GC 中被一并回收。 - 非实时,有暂停(GC pause):标记和清除过程会短暂阻塞 JavaScript 执行,尤其在堆内存大、活跃对象多时可能影响响应速度(V8 通过增量标记、并发标记等优化缓解)。
- 不依赖引用数字,逻辑更可靠:判断依据是“是否可达”,而非“被引用几次”,避免了计数维护本身的开销和异常场景。
引用计数:简单但有致命缺陷
该策略为每个对象维护一个引用计数器,每次新增引用+1,解除引用-1;当计数归零即立即回收。
- 实现简单、回收及时:没有明显停顿,适合对延迟敏感的旧环境(如某些嵌入式 JS 引擎)。
- 无法处理循环引用:上面 objA ↔ objB 的例子中,两者引用计数始终为 1,即使外部已无任何变量指向它们,内存也无法释放——这是它被主流引擎淘汰的主因。
- 计数操作开销不可忽略:赋值、作用域退出、属性删除等都要更新计数,频繁操作(如大量对象属性读写)会拖慢性能。
为什么 V8 等引擎只用标记清除(不混合)
虽然理论上可将引用计数作为辅助快速回收短期对象,但实际中得不偿失:
立即学习“Java免费学习笔记(深入)”;
- 现代标记清除已足够高效(如 V8 的 Scavenger(新生代)用复制算法 + Main GC(老生代)用标记清除/整理)。
- 引入引用计数会增加引擎复杂度,且无法根治循环引用问题,反而带来额外维护成本和潜在 bug。
- 开发者可通过弱引用(
WeakMap/WeakRef)手动控制部分引用关系,避免意外强引用延长对象生命周期。
对开发者的实际启示
你不需要手动触发 GC,但需注意哪些行为会影响标记清除的效果:
- 及时解除全局变量、事件监听器、定时器对 DOM 或大对象的引用(例如用
removeEventListener、clearTimeout)。 - 避免无意创建长生命周期闭包(如把大数组存在回调函数外层作用域中)。
- 用
WeakMap存储私有数据,它不阻止键对象被回收,适合做缓存或元数据绑定。 - 调试内存问题时,用 Chrome DevTools 的 Memory 面板拍快照,对比“Retained Size”和引用链,定位未被释放的根源。











