JavaScript 垃圾回收机制
JavaScript 采用自动垃圾回收(GC),开发者无需手动管理内存,但理解其原理对性能优化至关重要。
垃圾回收基本概念
什么是垃圾
不再被引用的对象就是垃圾,需要被回收。
JavaScript
let obj = { name: 'test' };
obj = null; // { name: 'test' } 不再被引用,成为垃圾
可达性分析
从根对象(全局变量、当前栈变量)出发,无法到达的对象即为垃圾。
JavaScript
// 根对象
// - 全局变量
// - 当前调用栈中的变量
function test() {
let a = { name: 'a' }; // a 可达
let b = { name: 'b' }; // b 可达
a.ref = b; // b 被 a 引用
b = null; // b 仍通过 a.ref 可达
a = null; // a 和 b 都不可达,可被回收
}
回收算法
标记-清除(Mark-Sweep)
JavaScript
┌─────────────────────────────────────┐
│ 标记-清除流程 │
├─────────────────────────────────────┤
│ 1. 标记阶段:从根遍历,标记可达对象 │
│ 2. 清除阶段:回收未标记对象 │
└─────────────────────────────────────┘
JavaScript
// 标记过程
const root = { a: 1 };
root.b = { c: 2 };
// 从 root 出发:
// root 可达 ✓
// root.a 可达 ✓
// root.b 可达 ✓
// root.b.c 可达 ✓
// 其他对象不可达,将被清除
标记-整理(Mark-Compact)
JavaScript
┌─────────────────────────────────────┐
│ 标记-整理流程 │
├─────────────────────────────────────┤
│ 1. 标记阶段:标记活动对象 │
│ 2. 整理阶段:移动对象,消除碎片 │
└─────────────────────────────────────┘
引用计数(Reference Counting)
JavaScript
// 记录每个对象的引用次数
let obj = { name: 'test' }; // 引用计数:1
let ref = obj; // 引用计数:2
obj = null; // 引用计数:1
ref = null; // 引用计数:0 → 可回收
// ⚠️ 循环引用问题
function createCycle() {
const a = {};
const b = {};
a.ref = b;
b.ref = a; // 循环引用,引用计数无法回收
}
现代浏览器使用标记-清除,不再使用引用计数。
V8 分代回收
分代假说
- 新生代:大部分对象生命周期短
- 老生代:少数对象生命周期长
内存布局
JavaScript
┌─────────────────────────────────────────────┐
│ V8 堆内存 │
├──────────────────┬──────────────────────────┤
│ 新生代 │ 老生代 │
│ (New Space) │ (Old Space) │
├──────────────────┼──────────────────────────┤
│ Semi-space │ Pointer Space │
│ From │ To │ Data Space │
│ 1-8MB │ 较大 │
├──────────────────┴──────────────────────────┤
│ 大对象空间 (Large Object Space) │
└─────────────────────────────────────────────┘
新生代回收(Scavenge)
JavaScript
┌─────────────────────────────────────┐
│ Scavenge 算法 │
├─────────────────────────────────────┤
│ From-Space → To-Space 复制活动对象 │
│ 交换 From 和 To │
│ 清空原 From(全是垃圾) │
└─────────────────────────────────────┘
JavaScript
// 新生代分配
function createObjects() {
const a = { x: 1 }; // 新生代
const b = { y: 2 }; // 新生代
return a; // a 晋升老生代,b 被回收
}
对象晋升
JavaScript
// 晋升条件
// 1. 经历过一次 Scavenge 回收
// 2. To-Space 使用超过 25%
function longLivingObject() {
const obj = {}; // 新生代
// 经历多次 GC 后晋升到老生代
return obj;
}
老生代回收(Mark-Sweep-Compact)
JavaScript
// 标记-清除-整理
// 1. 标记所有活动对象
// 2. 清除未标记对象
// 3. 整理碎片(可选)
增量标记与并发回收
全停顿问题
JavaScript
// 传统 GC 会暂停应用
// ┌────────────────────────────────┐
// │ 应用执行 │ GC 暂停 │ 应用执行 │
// │ │ 100ms │ │
// └────────────────────────────────┘
增量标记
JavaScript
// 将 GC 分成多个小任务
// ┌──────────────────────────────────────────┐
// │ 应用 │ GC │ 应用 │ GC │ 应用 │ GC │ 应用 │
// │ │ 1 │ │ 2 │ │ 3 │ │
// └──────────────────────────────────────────┘
// 减少单次停顿时间
并发回收
JavaScript
// GC 在后台线程执行,不阻塞主线程
// 主线程 │ 辅助线程
// ────────────────────┼────────────────
// 应用执行 │
// 应用执行 │ GC 标记
// 应用执行 │ GC 标记
// 应用执行 │ GC 清除
内存泄漏检测
Chrome DevTools
- 打开 Memory 面板
- 选择 Heap Snapshot
- 对比快照发现增长对象
Performance Monitor
JavaScript
// 监控内存使用
if (process.memoryUsage) {
setInterval(() => {
const used = process.memoryUsage();
console.log({
rss: `${(used.rss / 1024 / 1024).toFixed(2)} MB`,
heapTotal: `${(used.heapTotal / 1024 / 1024).toFixed(2)} MB`,
heapUsed: `${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`,
external: `${(used.external / 1024 / 1024).toFixed(2)} MB`
});
}, 1000);
}
常见泄漏模式
text
// 1. 全局变量
function leak() {
leakedGlobal = 'I am leaked'; // 挂载到 window
}
// 2. 闭包
function createClosure() {
const bigData = new Array(1000000);
return () => console.log(bigData.length); // 持续引用
}
// 3. DOM 引用
const elements = [];
document.getElementById('btn').addEventListener('click', () => {
elements.push(document.createElement('div')); // 持续积累
});
// 4. 定时器
setInterval(() => {
// 持续运行,引用外部变量
}, 1000);
GC 优化建议
减少垃圾产生
text
// ❌ 频繁创建对象
function animate() {
const point = { x: 0, y: 0 }; // 每帧创建
requestAnimationFrame(animate);
}
// ✅ 复用对象
const point = { x: 0, y: 0 };
function animate() {
point.x = 0;
point.y = 0;
requestAnimationFrame(animate);
}
使用对象池
text
class ObjectPool {
constructor(factory, reset) {
this.pool = [];
this.factory = factory;
this.reset = reset;
}
acquire() {
return this.pool.pop() || this.factory();
}
release(obj) {
this.reset(obj);
this.pool.push(obj);
}
}
避免隐藏类转换
text
// ❌ 不同属性顺序
const a = { x: 1, y: 2 };
const b = { y: 2, x: 1 }; // 不同 Hidden Class
// ✅ 相同属性顺序
const a = { x: 1, y: 2 };
const b = { x: 1, y: 2 }; // 相同 Hidden Class
要点总结
| 概念 | 说明 |
|---|---|
| 可达性 | 从根出发能否到达对象 |
| 标记-清除 | 标记活动对象,清除垃圾 |
| 分代回收 | 新生代用 Scavenge,老生代用 Mark-Sweep |
| 增量标记 | 分批标记,减少停顿 |
| 并发回收 | 后台 GC,不阻塞主线程 |
- 现代浏览器使用标记-清除算法
- 新生代小,回收频繁;老生代大,回收稀少
- 避免全局变量、闭包、定时器导致的泄漏
- 使用 DevTools 监控内存
- 对象池可减少 GC 压力
📝 发现内容有误?点击此处直接编辑