JavaScript中的垃圾回收算法:标记清除与引用计数
字数 885 2025-11-12 20:50:39
JavaScript中的垃圾回收算法:标记清除与引用计数
描述
JavaScript使用自动垃圾回收机制来管理内存,主要采用标记清除算法,历史上曾使用引用计数算法。理解这两种算法的工作原理和区别,对于避免内存泄漏和优化内存使用至关重要。
详细讲解
1. 垃圾回收的基本概念
- 垃圾回收是自动管理内存的过程,识别不再使用的内存并释放它
- 目标:防止内存泄漏,提高内存利用率
- JavaScript引擎在后台周期性执行垃圾回收
2. 引用计数算法(早期算法)
工作原理:
- 每个对象维护一个引用计数器,记录被引用的次数
- 当引用次数为0时,对象被视为垃圾并被回收
示例说明:
// 创建对象,引用计数为1
let obj1 = { name: '张三' };
// 增加引用,计数变为2
let obj2 = obj1;
// 减少引用,计数变回1
obj2 = null;
// 减少引用,计数变为0,对象被回收
obj1 = null;
循环引用问题:
function createCycle() {
let objA = { name: 'A' };
let objB = { name: 'B' };
// 相互引用,形成循环
objA.ref = objB; // objB引用计数:2
objB.ref = objA; // objA引用计数:2
}
createCycle();
// 函数执行完后,objA和objB的引用计数仍为1(相互引用)
// 无法被回收,导致内存泄漏
3. 标记清除算法(现代主流算法)
工作原理分步说明:
步骤1:标记根对象
- 从根对象(全局变量、当前执行上下文中的变量等)开始遍历
- 根对象包括:
- 全局对象(window/global)
- 当前函数调用栈中的变量和参数
- 活跃的DOM元素引用
步骤2:标记可达对象
- 从根对象出发,深度优先遍历所有引用的对象
- 所有被访问到的对象标记为"可达"
步骤3:清除不可达对象
- 扫描堆内存中的所有对象
- 删除未被标记为"可达"的对象
- 释放对应的内存空间
示例演示:
function demo() {
let obj1 = { data: '重要数据' }; // 可达:被局部变量引用
let obj2 = { data: '临时数据' }; // 可达:被局部变量引用
// 创建循环引用
obj1.ref = obj2;
obj2.ref = obj1;
}
demo(); // 函数执行结束
// 垃圾回收过程:
// 1. 根对象中不再包含obj1和obj2的引用
// 2. 虽然obj1和obj2相互引用,但从根对象不可达
// 3. 两者都被标记为不可达,被回收
4. 两种算法的对比
| 特性 | 引用计数 | 标记清除 |
|---|---|---|
| 实时性 | 立即回收 | 周期性回收 |
| 性能影响 | 每次引用变化时计算 | 执行时可能暂停脚本 |
| 循环引用 | 无法处理 | 可以正确处理 |
| 实现复杂度 | 相对简单 | 相对复杂 |
5. 现代优化技术
分代收集:
- 将对象分为"新生代"和"老生代"
- 新生代:新创建的对象,回收频繁
- 老生代:存活时间长的对象,回收较少
增量标记:
- 将标记过程分成小步骤执行
- 避免长时间阻塞主线程
空闲时收集:
- 在浏览器空闲时段执行垃圾回收
- 减少对用户体验的影响
6. 开发中的注意事项
避免内存泄漏的最佳实践:
// 1. 及时清除定时器
let timer = setInterval(() => {}, 1000);
// 使用后清除
clearInterval(timer);
// 2. 移除事件监听器
element.addEventListener('click', handler);
// 需要时移除
element.removeEventListener('click', handler);
// 3. 避免意外的全局变量
function leak() {
leakedVar = '这会泄漏到全局'; // 错误
let safeVar = '这是局部的'; // 正确
}
// 4. 清除DOM引用
let elements = document.querySelectorAll('.items');
// 使用后置空
elements = null;
理解这些垃圾回收机制有助于编写更高效、更安全的JavaScript代码,避免常见的内存管理问题。