浏览器的垃圾回收(Garbage Collection,GC)机制是JavaScript引擎自动管理内存的核心机制,用于识别和回收不再使用的内存空间,防止内存泄漏。
一、垃圾回收的基本原理
1. 核心概念
- 可达性(Reachability) :从根对象(全局变量、活动函数局部变量等)出发,能够访问到的对象就是“可达的”,需要保留
- 不可达对象:无法从根对象访问到的对象,被视为垃圾,可以被回收
2. 根对象(Roots)
- 全局对象(window/global)
- 当前函数的局部变量和参数
- 嵌套调用链上的其他函数的变量
- DOM元素引用
- 其他全局对象
二、主要的垃圾回收算法
1. 标记-清除算法(Mark-and-Sweep) (最常用)
// 示例:对象引用关系
let obj1 = { name: 'obj1' }; // 可达
let obj2 = obj1; // 可达
obj1 = null; // obj2仍然可达
// obj2 = null; // 如果取消注释,对象变为不可达
过程:
- 标记阶段:从根对象开始,标记所有可达对象
- 清除阶段:遍历堆内存,清除未标记的对象
2. 引用计数算法(Reference Counting) (逐渐被淘汰)
// 问题:循环引用
function problem() {
let objA = {}; // objA引用计数:1
let objB = {}; // objB引用计数:1
objA.ref = objB; // objB引用计数:2
objB.ref = objA; // objA引用计数:2
// 函数结束,objA和objB局部引用消失
// objA引用计数:1,objB引用计数:1
// 但两者都无法被访问,却无法回收 - 内存泄漏!
}
三、现代浏览器的优化策略
1. 分代回收(Generational Collection)
-
新生代(Young Generation) :新创建的对象
- 使用Scavenge算法(复制算法)
- 分为From空间和To空间
- 回收频繁,速度快
-
老生代(Old Generation) :存活时间长的对象
- 使用标记-清除/标记-整理算法
- 回收频率低,但耗时较长
2. 增量回收(Incremental Collection)
- 将完整的GC过程分成多个小步骤
- 与JavaScript执行交替进行
- 减少单次GC的停顿时间
3. 空闲时间回收(Idle-time Collection)
- 利用浏览器的空闲时间进行GC
- Chrome的Idle Until Urgent机制
四、V8引擎的垃圾回收(以Chrome为例)
1. 新生代回收:Scavenge算法
// 新生代空间(通常1-8MB)
// 对象晋升条件:
// 1. 经历过一次Scavenge回收
// 2. To空间使用超过25%
// 满足任一条件则晋升到老生代
2. 老生代回收:三色标记法
- 白色:未被访问的对象(待回收)
- 灰色:已被访问,但子引用未检查
- 黑色:已被访问,且子引用已检查
3. 并行和并发回收
- 并行回收:主线程暂停,多个辅助线程并行GC
- 并发回收:辅助线程并发执行GC,主线程继续执行JS
五、内存泄漏常见场景及避免方法
1. 意外的全局变量
// ❌ 错误:创建了全局变量
function leak() {
leaked = 'I am global'; // 没有var/let/const
this.globalVar = 'oops'; // 非严格模式下,this指向window
}
// ✅ 正确
function safe() {
'use strict';
let local = 'I am local';
}
2. 遗忘的定时器和回调
// ❌ 可能泄漏
let data = getHugeData();
setInterval(() => {
const node = document.getElementById('node');
if (node) {
node.innerHTML = JSON.stringify(data); // data被闭包引用
}
}, 1000);
// ✅ 及时清理
const timer = setInterval(() => { /* ... */ }, 1000);
clearInterval(timer); // 不需要时清理
3. DOM引用
// ❌ 泄漏
let elements = {
button: document.getElementById('button'),
image: document.getElementById('image')
};
// 即使从DOM移除,JS引用仍然存在
document.body.removeChild(document.getElementById('button'));
// ✅ 清理引用
elements.button = null;
4. 闭包
// ❌ 不当使用闭包
function outer() {
let largeArray = new Array(1000000).fill('*');
return function inner() {
console.log('hello'); // 引用了largeArray
};
}
// ✅ 适当释放
function outer() {
let largeArray = new Array(1000000).fill('*');
// 使用后置null
let result = process(largeArray);
largeArray = null; // 帮助GC
return result;
}
六、开发者工具中的内存分析
1. Chrome DevTools Memory工具
- Heap Snapshot:堆内存快照
- Allocation Timeline:内存分配时间线
- Allocation Sampling:内存分配采样
2. 性能监控
// 监测内存使用
setInterval(() => {
const memory = performance.memory;
console.log({
usedJSHeapSize: memory.usedJSHeapSize / 1048576 + 'MB',
totalJSHeapSize: memory.totalJSHeapSize / 1048576 + 'MB',
jsHeapSizeLimit: memory.jsHeapSizeLimit / 1048576 + 'MB'
});
}, 5000);
七、最佳实践
- 及时解除引用:不再使用的对象设为
null - 避免创建全局变量:使用严格模式
- 谨慎使用闭包:避免不必要的大型对象被闭包引用
- 清理事件监听器:使用
removeEventListener - 使用WeakMap/WeakSet:弱引用不阻止垃圾回收
- 分批处理大数据:避免一次性操作大量数据
八、不同浏览器的差异
| 浏览器 | 引擎 | 主要GC算法 | 特点 |
|---|---|---|---|
| Chrome | V8 | 分代 + 增量 + 并行 | 性能最好,GC策略最复杂 |
| Firefox | SpiderMonkey | 分代 + 增量 | 采用Zeal GC,逐步优化 |
| Safari | JavaScriptCore | 分代 + 并发 | 低延迟GC |
| Edge | Chakra(旧)/V8(新) | 类似V8 | 新版Edge已使用V8 |
浏览器的垃圾回收机制是不断优化的过程,现代浏览器都在努力减少GC暂停时间,提高应用性能。作为开发者,理解这些原理有助于编写更高效、更少内存泄漏的代码。