垃圾回收机制(Garbage Collection)
什么是垃圾回收?
垃圾回收(GC)是一种自动内存管理机制,用于回收程序中不再使用的内存空间。JavaScript 引擎会自动追踪内存分配和使用情况,并在适当的时候释放不再需要的内存。
垃圾回收的基本概念
可达性(Reachability)
垃圾回收器通过"可达性"来判断对象是否还在使用:
- 可达对象:从根(全局对象、当前执行栈等)出发,可以通过引用链访问到的对象
- 不可达对象:无法从根访问到的对象,被视为垃圾
根对象(Roots)
以下对象始终被认为是可达的:
- 全局对象(浏览器中的
window,Node.js 中的global) - 当前执行栈中的局部变量和参数
- 活动函数的闭包变量
- 正在执行的函数
垃圾回收算法
1. 引用计数(Reference Counting)
原理:为每个对象维护一个引用计数器,记录有多少个引用指向该对象。
javascript
// 引用计数示例
let obj1 = { name: 'Alice' }; // obj1的引用计数:1
let obj2 = obj1; // obj1的引用计数:2
obj1 = null; // obj1的引用计数:1
obj2 = null; // obj1的引用计数:0,可以被回收优点:
- 实现简单
- 回收及时
缺点:
- 无法解决循环引用问题
- 需要额外空间存储计数器
- 频繁更新计数器影响性能
javascript
// 循环引用问题
function circularReference() {
let obj1 = {};
let obj2 = {};
obj1.ref = obj2; // obj1引用obj2
obj2.ref = obj1; // obj2引用obj1
return null;
}
circularReference();
// obj1和obj2互相引用,引用计数永远不为0
// 即使函数执行完毕,这两个对象也无法被回收2. 标记-清除(Mark-and-Sweep)★
原理:这是 JavaScript 最常用的垃圾回收算法。
执行过程:
- 标记阶段:从根对象开始遍历,标记所有可达对象
- 清除阶段:遍历堆内存,回收未被标记的对象
javascript
// 标记-清除示例
function createObjects() {
let obj1 = { data: 'important' }; // 创建对象
let obj2 = { data: 'temporary' }; // 创建对象
globalThis.keepAlive = obj1; // obj1可达(通过全局对象)
return null;
}
createObjects();
// GC执行时:
// 1. 从根开始标记:globalThis -> keepAlive -> obj1 ✓
// 2. obj2不可达,未被标记
// 3. 清除阶段回收obj2优点:
- 可以解决循环引用问题
- 不需要额外的引用计数
缺点:
- 回收时会暂停程序执行(Stop-the-World)
- 内存碎片化问题
3. 标记-整理(Mark-Compact)
原理:标记-清除的改进版,在清除后会整理内存。
执行过程:
- 标记所有可达对象
- 将所有存活对象移动到内存的一端
- 清除边界外的内存
优点:
- 解决内存碎片问题
- 提高内存利用率
缺点:
- 移动对象需要更新所有引用
- 执行时间较长
4. 复制算法(Copying)
原理:将内存分为两个区域,每次只使用一个区域。
执行过程:
- 将存活对象从当前区域复制到另一个区域
- 清空当前区域
- 交换两个区域的角色
优点:
- 没有内存碎片
- 分配内存快速(指针碰撞)
缺点:
- 内存利用率只有50%
- 复制对象有开销
分代回收(Generational GC)
现代 JavaScript 引擎采用分代回收策略,基于"大部分对象生命周期很短"的观察。
新生代(Young Generation/New Space)
特点:
- 存放新创建的对象
- 空间较小(1-8MB)
- GC 频率高
- 采用 Scavenge 算法(复制算法的变种)
javascript
// 新生代对象示例
function processData() {
let temp = { data: [] }; // 临时对象,生命周期短
for (let i = 0; i < 1000; i++) {
temp.data.push(i);
}
return temp.data.length;
}
// temp对象在函数执行完后很快被回收Scavenge 算法:
- 新生代分为两个空间:From 空间和 To 空间
- 对象首先分配在 From 空间
- GC 时将存活对象复制到 To 空间
- 清空 From 空间,交换两个空间
老生代(Old Generation/Old Space)
特点:
- 存放生命周期长的对象
- 空间较大
- GC 频率低
- 采用标记-清除和标记-整理算法
javascript
// 老生代对象示例
const cache = new Map(); // 长期存在的缓存对象
function addToCache(key, value) {
cache.set(key, value); // 这些对象可能晋升到老生代
}对象晋升条件:
- 对象在新生代经历过一次 GC 仍然存活
- To 空间使用超过 25%
增量标记(Incremental Marking)
目的:减少 GC 造成的停顿时间
原理:将标记过程分解为多个小步骤,与 JavaScript 执行交替进行
JavaScript 执行 → 增量标记 → JavaScript 执行 → 增量标记 → ...优点:
- 避免长时间停顿
- 提高应用响应性
挑战:
- 标记过程中对象引用可能发生变化
- 需要写屏障(Write Barrier)追踪变化
并发标记(Concurrent Marking)
原理:标记过程在后台线程中执行,与 JavaScript 主线程并发运行
优点:
- 进一步减少主线程停顿
- 充分利用多核 CPU
空闲时间回收(Idle-time GC)
利用浏览器的空闲时间(如 requestIdleCallback)执行垃圾回收,避免影响用户交互。
内存泄漏常见场景
1. 意外的全局变量
javascript
// 不良实践
function createLeak() {
leak = { data: new Array(1000000) }; // 缺少 var/let/const
}
// leak 成为全局变量,永远不会被回收2. 遗忘的定时器
javascript
// 不良实践
function setupTimer() {
let largeData = new Array(1000000);
setInterval(() => {
console.log(largeData[0]);
}, 1000);
}
// 即使 setupTimer 执行完毕,largeData 仍被定时器引用解决方案:
javascript
// 良好实践
function setupTimer() {
let largeData = new Array(1000000);
const timer = setInterval(() => {
console.log(largeData[0]);
}, 1000);
// 在适当时机清除定时器
return () => clearInterval(timer);
}
const cleanup = setupTimer();
// 不再需要时
cleanup();3. 闭包引用
javascript
// 不良实践
function createClosure() {
let largeData = new Array(1000000);
return function() {
console.log('closure');
// 即使不使用 largeData,它仍被闭包引用
};
}
const fn = createClosure();
// largeData 无法被回收解决方案:
javascript
// 良好实践
function createClosure() {
let largeData = new Array(1000000);
let smallData = largeData[0];
largeData = null; // 手动释放
return function() {
console.log(smallData);
};
}4. DOM 引用
javascript
// 不良实践
let elements = [];
function addElements() {
let div = document.createElement('div');
document.body.appendChild(div);
elements.push(div);
}
function removeElements() {
document.body.innerHTML = '';
// elements 数组仍然引用已删除的 DOM 节点
}解决方案:
javascript
// 良好实践
function removeElements() {
document.body.innerHTML = '';
elements = []; // 清空引用数组
}5. 事件监听器
javascript
// 不良实践
function attachListener() {
let button = document.getElementById('btn');
let largeData = new Array(1000000);
button.addEventListener('click', function() {
console.log(largeData.length);
});
}
// 移除 button 后,监听器仍引用 largeData解决方案:
javascript
// 良好实践
function attachListener() {
let button = document.getElementById('btn');
let largeData = new Array(1000000);
function handler() {
console.log(largeData.length);
}
button.addEventListener('click', handler);
// 清理函数
return () => {
button.removeEventListener('click', handler);
largeData = null;
};
}性能优化建议
1. 及时释放引用
javascript
// 使用完大对象后及时置为 null
let largeArray = new Array(1000000);
// ... 使用 largeArray
largeArray = null; // 帮助 GC2. 使用对象池
javascript
// 对象池模式
class ObjectPool {
constructor(createFn, resetFn) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
}
acquire() {
return this.pool.pop() || this.createFn();
}
release(obj) {
this.resetFn(obj);
this.pool.push(obj);
}
}
// 使用示例
const vectorPool = new ObjectPool(
() => ({ x: 0, y: 0 }),
(v) => { v.x = 0; v.y = 0; }
);3. 避免频繁创建临时对象
javascript
// 不良实践
function calculate(arr) {
return arr.map(x => ({ value: x * 2 })) // 创建大量临时对象
.filter(obj => obj.value > 10)
.map(obj => obj.value);
}
// 良好实践
function calculate(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
const value = arr[i] * 2;
if (value > 10) {
result.push(value);
}
}
return result;
}4. 使用 WeakMap 和 WeakSet
javascript
// WeakMap 不会阻止键对象被垃圾回收
const cache = new WeakMap();
function processObject(obj) {
if (!cache.has(obj)) {
cache.set(obj, expensiveComputation(obj));
}
return cache.get(obj);
}
// 当 obj 不再被使用时,cache 中的条目会自动被清理监控和调试工具
Chrome DevTools
Performance 面板:
- 查看 GC 事件
- 分析 GC 造成的停顿
Memory 面板:
- 堆快照(Heap Snapshot)
- 内存分配时间线
- 对比快照查找内存泄漏
Node.js
javascript
// 获取内存使用情况
console.log(process.memoryUsage());
// {
// rss: 25165824, // 常驻集大小
// heapTotal: 6537216, // 堆总大小
// heapUsed: 4345408, // 已使用堆大小
// external: 1234567, // C++ 对象绑定的内存
// arrayBuffers: 12345 // ArrayBuffer 和 SharedArrayBuffer
// }
// 手动触发 GC(需要 --expose-gc 启动)
if (global.gc) {
global.gc();
}总结
- JavaScript 使用自动垃圾回收,主要采用标记-清除算法
- 现代引擎使用分代回收、增量标记、并发标记等优化技术
- 理解 GC 机制有助于写出高性能代码
- 注意避免常见的内存泄漏场景
- 使用 DevTools 监控和调试内存问题