javascript早期引用计数垃圾回收因无法处理循环引用而被弃用;它通过跟踪引用次数回收对象,但无法识别互相引用却外部不可达的对象组,现代引擎改用标记-清除为主、引用计数为辅的混合策略。

JavaScript早期的引用计数垃圾回收算法因无法处理循环引用,导致内存泄漏风险显著——这是它被V8等现代引擎弃用的核心原因。
引用计数的基本逻辑
该算法为每个值维护一个“被引用次数”:每当有变量、属性或闭包指向该对象,计数加1;当引用被移除(如赋值为null、变量离开作用域),计数减1。一旦计数归零,对象立即被回收。
这种机制直观高效,适合简单场景,但存在根本性盲区:
- 无法识别“互相引用但外部已不可达”的对象组
- 不追踪对象间的可达路径,只看孤立的引用数量
- 对闭包、DOM事件监听器等隐式引用缺乏感知
经典循环引用:对象互持与DOM绑定
两个对象彼此持有对方的引用,即使整个结构已脱离全局作用域,它们的引用计数仍大于0,无法被回收。
例如:
function createCircularRef() {
const objA = {};
const objB = {};
objA.refToB = objB;
objB.refToA = objA;
return objA; // 返回后,objA和objB在函数外无任何外部引用
}
createCircularRef(); // 此时objA和objB仍互相引用,引用计数均为1
在纯引用计数下,这两个对象永远不会被释放。
更常见的是JS对象与DOM节点之间的循环引用:
const element = document.getElementById('myDiv');
const handler = function() { console.log('clicked'); };
element.addEventListener('click', handler);
element.jsReference = handler; // DOM节点引用JS函数
handler.domReference = element; // JS函数反向引用DOM节点
即使页面跳转或元素被remove(),若未手动清理事件监听器和自定义属性,这对引用就会长期驻留内存。
现代引擎如何绕过这个问题
V8、SpiderMonkey等引擎早已放弃纯引用计数,转而采用标记-清除(Mark-and-Sweep)为主、辅以引用计数优化的混合策略。
标记-清除从一组“根对象”(如全局对象、当前执行上下文中的变量)出发,递归标记所有可达对象;未被标记的对象即为不可达,统一回收。它天然能识别并清理循环引用结构。
不过,引用计数并未完全消失——它仍被用于优化某些场景(如快速释放确定无共享的局部对象),但不再作为主回收机制。
开发者仍需防范的“伪循环引用”场景
即便引擎已解决底层循环引用问题,不当编码仍会制造事实上的内存泄漏:
- 未解绑的事件监听器:尤其在单页应用中反复创建组件却忘记removeEventListener
- 全局缓存未清理:将DOM节点、大型数据结构存入window.cache或模块级Map,且无淘汰策略
- 闭包意外捕获大对象:定时器、Promise回调中持续持有本应释放的数组或JSON数据
- WeakMap/WeakSet误用:以为用了弱引用就绝对安全,却忽略了键必须是对象、且弱引用只针对键本身
这些不是引用计数缺陷的复现,而是可达性控制失当的结果——根源仍在开发者对对象生命周期的理解偏差。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











