什么是垃圾回收?
垃圾回收(Garbage Collection)是 JavaScript 引擎自动管理内存的机制,它会定期找出不再使用的变量和对象,并释放它们占用的内存空间。
主要垃圾回收算法
1. 引用计数(Reference Counting) - 已淘汰
工作原理:跟踪每个值被引用的次数
ini
let obj1 = { name: 'Alice' }; // 引用计数: 1
let obj2 = obj1; // 引用计数: 2
obj1 = null; // 引用计数: 1
obj2 = null; // 引用计数: 0 → 可回收
循环引用问题:
ini
function problem() {
let objA = {};
let objB = {};
objA.ref = objB; // objA 引用 objB
objB.ref = objA; // objB 引用 objA
// 即使函数执行完毕,引用计数都不为0,无法回收!
}
2. 标记-清除(Mark-and-Sweep) - 现代主流
工作原理:从根对象开始标记所有可达对象,清除未标记对象
csharp
// 根对象(全局变量、当前函数局部变量等)
// ↓ 标记阶段:遍历所有从根对象可达的对象
// ↓ 清除阶段:回收不可达对象
function example() {
let obj1 = { data: 'temp' }; // 可达
let obj2 = { related: obj1 }; // 可达
obj1.connection = obj2; // 互相引用,但都可达
}
// 函数执行完毕后,obj1 和 obj2 都不可达 → 被回收
现代 V8 引擎的垃圾回收机制
代际假说(Generational Hypothesis)
- 大部分对象生存时间很短
- 少数对象会存活很长时间
基于这个假说,V8 将堆内存分为两个区域:
1. 新生代(Young Generation)
- 存放新创建的对象
- 空间小(1-8MB),垃圾回收频繁
- 使用 Scavenge 算法
Scavenge 算法过程:
vbnet
// 新生代分为两个半空间:From 和 To
function scavengeExample() {
// 新对象分配在 From 空间
let newObj1 = { id: 1 }; // From 空间
let newObj2 = { id: 2 }; // From 空间
// 当 From 空间快满时,执行 Scavenge:
// 1. 将存活对象复制到 To 空间
// 2. 清空 From 空间
// 3. 交换 From 和 To 角色
}
对象晋升:
csharp
function objectPromotion() {
let tempObj = { data: 'temporary' };
// 第一次 GC:tempObj 存活 → 复制到 To 空间
// 第二次 GC:tempObj 仍然存活 → 晋升到老生代
// 后续多次存活的对象会晋升到老生代
}
2. 老生代(Old Generation)
- 存放存活时间较长的对象
- 空间大,垃圾回收不那么频繁
- 使用 标记-清除 和 标记-整理 算法
ini
let longLiveObj = { important: 'data' }; // 长期存活的对象
function createManyObjects() {
for (let i = 0; i < 1000; i++) {
let temp = { index: i }; // 大部分很快被回收
if (i === 500) {
longLiveObj.ref = temp; // 让某个临时对象存活更久
}
}
}
垃圾回收的触发时机
1. 自动触发
javascript
// 以下情况可能触发 GC:
function autoGC() {
// 1. 分配新对象时空间不足
let bigArray = new Array(1000000);
// 2. 定时执行(不同浏览器策略不同)
setTimeout(() => {
let anotherArray = new Array(100000);
}, 1000);
}
2. 手动触发(谨慎使用)
csharp
// 非标准方法,主要用于调试
if (typeof global.gc === 'function') {
global.gc(); // Node.js 中手动触发 GC
}
// 浏览器中(Chrome)
// 打开开发者工具 → Memory → 点击垃圾箱图标
内存泄漏的常见模式及避免方法
1. 意外的全局变量
csharp
// ❌ 错误做法
function leak1() {
leakedVar = '这是一个全局变量'; // 忘记 var/let/const
}
// ✅ 正确做法
function noLeak1() {
let localVar = '局部变量';
}
2. 遗忘的定时器和回调
kotlin
// ❌ 内存泄漏
class Component {
constructor() {
this.data = largeData;
this.timer = setInterval(() => {
this.update();
}, 1000);
}
// 忘记清理定时器!
}
// ✅ 正确做法
class SafeComponent {
constructor() {
this.data = largeData;
this.timer = setInterval(() => {
this.update();
}, 1000);
}
destroy() {
clearInterval(this.timer); // 及时清理
this.data = null; // 解除引用
}
}
3. DOM 引用泄漏
javascript
// ❌ 泄漏
const elements = {
button: document.getElementById('myButton'),
container: document.getElementById('container')
};
// 即使从 DOM 移除,JS 引用仍然存在
document.body.removeChild(elements.container);
// ✅ 正确做法
function cleanUp() {
elements.button = null;
elements.container = null;
}
4. 闭包引用
javascript
// ❌ 可能泄漏
function createClosure() {
const bigData = new Array(1000000);
return function() {
// 闭包持有 bigData 的引用,即使不再需要
console.log('closure executed');
};
}
// ✅ 优化版本
function optimizedClosure() {
const bigData = new Array(1000000);
// 使用完后显式释放
const result = processData(bigData);
bigData.length = 0; // 释放数组内存
return function() {
console.log(result);
};
}
性能优化建议
1. 对象池模式
kotlin
// ✅ 减少垃圾回收压力
class ObjectPool {
constructor(createFn) {
this.create = createFn;
this.pool = [];
}
get() {
return this.pool.length > 0 ? this.pool.pop() : this.create();
}
release(obj) {
// 重置对象状态而不是销毁
this.pool.push(obj);
}
}
// 使用对象池
const particlePool = new ObjectPool(() => ({ x: 0, y: 0, active: false }));
2. 避免内存抖动
ini
// ❌ 频繁创建销毁对象
function badPattern() {
for (let i = 0; i < 1000; i++) {
let tempObj = { index: i }; // 频繁 GC
process(tempObj);
}
}
// ✅ 重用对象
function goodPattern() {
let tempObj = {};
for (let i = 0; i < 1000; i++) {
tempObj.index = i; // 重用对象
process(tempObj);
}
}
调试内存问题
Chrome DevTools 内存分析
csharp
// 1. 创建内存快照
function createSnapshot() {
// 在 DevTools Memory 面板点击 "Take snapshot"
}
// 2. 内存分配时间线
function trackAllocations() {
// 使用 "Allocation instrumentation on timeline"
}
// 3. 检查分离的 DOM 节点
function checkDetachedDOM() {
// 查看 "Detached" 的 DOM 节点
}
总结
JavaScript 的垃圾回收机制让开发者无需手动管理内存,但理解其工作原理对于:
- 避免内存泄漏
- 优化性能
- 调试内存问题
- 编写高效代码
至关重要。记住关键原则:及时释放不再需要的引用,特别是在处理大型数据结构和长期运行的应用中。