JavaScript 垃圾回收机制
引用计数、标记清除和分代回收的原理与区别
问题
JavaScript 的垃圾回收机制是如何工作的?引用计数、标记清除、分代回收分别是什么?
解答
引用计数(Reference Counting)
早期的垃圾回收策略。每个对象维护一个引用计数,当计数为 0 时回收。
// 引用计数示例
let obj = { name: 'test' }; // { name: 'test' } 引用计数 = 1
let copy = obj; // { name: 'test' } 引用计数 = 2
obj = null; // { name: 'test' } 引用计数 = 1
copy = null; // { name: 'test' } 引用计数 = 0,被回收
致命问题:循环引用
function problem() {
let objA = {};
let objB = {};
objA.ref = objB; // objB 引用计数 = 2
objB.ref = objA; // objA 引用计数 = 2
// 函数结束后,objA 和 objB 引用计数都是 1
// 永远不会被回收 → 内存泄漏
}
标记清除(Mark-and-Sweep)
现代浏览器采用的主要算法。从根对象(全局对象、调用栈)出发,标记所有可达对象,清除未标记的对象。
// 标记清除过程示意
let root = {
child: {
grandchild: { value: 1 }
}
};
// 阶段1:标记
// 从 root 开始遍历,标记 root → child → grandchild
// 阶段2:清除
// 未被标记的对象被回收
root.child = null;
// 下次 GC 时,原来的 child 和 grandchild 不可达,被回收
循环引用不再是问题:
function noLeak() {
let objA = {};
let objB = {};
objA.ref = objB;
objB.ref = objA;
}
noLeak();
// 函数执行完毕,objA 和 objB 从根对象不可达
// 即使互相引用,也会被回收
分代回收(Generational Collection)
V8 引擎的优化策略,基于”大多数对象生命周期很短”的假设。
┌─────────────────────────────────────────────────┐
│ V8 堆内存 │
├──────────────────────┬──────────────────────────┤
│ 新生代 │ 老生代 │
│ (1-8MB, 小) │ (大, 主要空间) │
├──────────────────────┼──────────────────────────┤
│ Scavenge 算法 │ Mark-Sweep-Compact │
│ 频繁、快速回收 │ 较少触发、耗时较长 │
└──────────────────────┴──────────────────────────┘
新生代回收(Scavenge):
// 新生代使用复制算法,空间分为 From 和 To 两部分
// 1. 新对象分配在 From 空间
let newObj = { data: 'new' };
// 2. From 空间满时触发 Scavenge
// - 存活对象复制到 To 空间
// - From 和 To 角色互换
// - 原 From 空间清空
// 3. 对象晋升到老生代的条件:
// - 经历过一次 Scavenge 仍存活
// - To 空间使用超过 25%
老生代回收(Mark-Sweep-Compact):
// 老生代使用标记清除 + 标记整理
// Mark-Sweep: 标记存活对象,清除死亡对象
// 问题:产生内存碎片
// Mark-Compact: 将存活对象移动到一端,整理碎片
// 代价:移动对象耗时
// V8 策略:主要用 Mark-Sweep,碎片过多时用 Mark-Compact
避免内存泄漏的实践
// 1. 及时清除定时器
const timer = setInterval(() => {}, 1000);
clearInterval(timer); // 不用时清除
// 2. 移除事件监听
const handler = () => {};
element.addEventListener('click', handler);
element.removeEventListener('click', handler); // 不用时移除
// 3. 避免意外的全局变量
function leak() {
leaked = 'oops'; // 没有声明,成为全局变量
}
// 4. 使用 WeakMap/WeakSet 存储对象引用
const cache = new WeakMap();
let obj = { data: 'value' };
cache.set(obj, 'metadata');
obj = null; // obj 可被回收,WeakMap 中的条目自动移除
关键点
- 引用计数:简单但有循环引用问题,现代浏览器已弃用
- 标记清除:从根对象遍历标记可达对象,解决了循环引用问题
- 分代回收:V8 将堆分为新生代和老生代,针对性优化回收效率
- 新生代用 Scavenge:复制算法,快速回收短命对象
- 老生代用 Mark-Sweep-Compact:标记清除 + 整理,处理长期存活对象
目录