垃圾回收(Garbage Collection,GC)是 JavaScript 引擎自动管理内存的机制,负责回收不再使用的对象占用的内存。
JavaScript 使用自动垃圾回收机制,开发者不需要手动管理内存。当对象不再被引用时,垃圾回收器会自动回收其占用的内存。
function createObject() {
const obj = { name: 'Alice' };
return obj;
}
const myObj = createObject();
// obj 在函数执行完毕后,如果没有被引用,会被回收
这是现代 JavaScript 引擎最常用的垃圾回收算法。
工作原理:
// 示例
let obj1 = { name: 'obj1' };
let obj2 = { name: 'obj2' };
obj1.ref = obj2;
obj2.ref = obj1;
obj1 = null;
obj2 = null;
// 两个对象形成循环引用,但标记清除算法可以处理
早期浏览器使用的算法,现在已很少使用。
工作原理:
问题:
// 循环引用问题
function createCycle() {
const obj1 = {};
const obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1; // 循环引用
return obj1;
}
const cycle = createCycle();
// 引用计数算法无法回收这两个对象
内存泄漏是指不再使用的内存没有被正确释放。
// 不好的做法
function leak() {
name = 'Alice'; // 创建全局变量
this.age = 25; // 在非严格模式下,this 指向全局对象
}
// 好的做法
function noLeak() {
const name = 'Alice';
const age = 25;
}
// 内存泄漏
function startTimer() {
setInterval(() => {
console.log('tick');
}, 1000);
}
// 正确的做法
let timerId;
function startTimer() {
timerId = setInterval(() => {
console.log('tick');
}, 1000);
}
function stopTimer() {
clearInterval(timerId);
}
// 内存泄漏
function attachListener() {
const button = document.getElementById('button');
button.addEventListener('click', function() {
console.log('clicked');
});
// 如果 button 被移除,监听器仍然存在
}
// 正确的做法
function attachListener() {
const button = document.getElementById('button');
const handler = function() {
console.log('clicked');
};
button.addEventListener('click', handler);
// 在适当时机移除
button.removeEventListener('click', handler);
}
// 可能导致内存泄漏
function createHandler() {
const largeData = new Array(1000000).fill(0);
return function() {
// 闭包持有 largeData 的引用
console.log('handler');
};
}
const handler = createHandler();
// largeData 不会被回收,因为被闭包引用
// 正确的做法
function createHandler() {
return function() {
console.log('handler');
};
// largeData 在函数执行完毕后可以被回收
}
// 内存泄漏
const elements = [];
function storeElements() {
const divs = document.querySelectorAll('div');
elements.push(...divs); // 保存 DOM 引用
}
// 即使 DOM 元素被移除,elements 数组仍然持有引用
// 正确的做法
const elementIds = [];
function storeElementIds() {
const divs = document.querySelectorAll('div');
divs.forEach(div => {
elementIds.push(div.id); // 只保存 ID,不保存 DOM 引用
});
}
V8 使用分代垃圾回收策略:
新生代(New Space)
老生代(Old Space)
// 新创建的对象在新生代
const obj = { name: 'Alice' };
// 经过多次垃圾回收后,存活的对象会晋升到老生代
let largeObject = createLargeObject();
// 使用完毕后
largeObject = null; // 帮助垃圾回收
// 不好的做法
function processData(data) {
return data.map(item => {
return {
id: item.id,
name: item.name,
processed: true
};
});
}
// 如果可能,直接修改原对象
function processData(data) {
data.forEach(item => {
item.processed = true;
});
return data;
}
// 对象池模式,重用对象
class ObjectPool {
constructor(createFn, resetFn) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
}
acquire() {
return this.pool.length > 0
? this.pool.pop()
: this.createFn();
}
release(obj) {
this.resetFn(obj);
this.pool.push(obj);
}
}
// 使用
const pool = new ObjectPool(
() => ({ x: 0, y: 0 }),
obj => { obj.x = 0; obj.y = 0; }
);
const obj = pool.acquire();
// 使用对象
pool.release(obj); // 归还到池中,而不是销毁
// 不好的做法
for (let i = 0; i < 1000; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 好的做法
function createHandler(i) {
return function() {
console.log(i);
};
}
for (let i = 0; i < 1000; i++) {
setTimeout(createHandler(i), 1000);
}
// WeakMap 的键是弱引用,不会阻止垃圾回收
const weakMap = new WeakMap();
const obj = {};
weakMap.set(obj, 'data');
// 当 obj 没有其他引用时,weakMap 中的条目会被自动清除
// 在控制台中使用
// performance.memory 可以查看内存使用情况
console.log(performance.memory);
// {
// usedJSHeapSize: 10000000,
// totalJSHeapSize: 20000000,
// jsHeapSizeLimit: 2000000000
// }
// 记录内存使用
const memoryBefore = performance.memory.usedJSHeapSize;
// 执行操作
doSomething();
const memoryAfter = performance.memory.usedJSHeapSize;
console.log(`内存使用: ${(memoryAfter - memoryBefore) / 1024 / 1024} MB`);
// Chrome DevTools 中可以使用
// 在控制台执行:window.gc()
// 需要启动 Chrome 时添加参数:--js-flags="--expose-gc"
// 标记清除算法可以处理循环引用
let obj1 = { name: 'obj1' };
let obj2 = { name: 'obj2' };
obj1.ref = obj2;
obj2.ref = obj1;
obj1 = null;
obj2 = null;
// 两个对象都会被回收(标记清除算法)
// WeakMap 的键必须是对象,且是弱引用
const weakMap = new WeakMap();
let obj = { id: 1 };
weakMap.set(obj, 'data');
obj = null; // obj 可以被回收,weakMap 中的条目也会被清除
// 普通 Map 会阻止垃圾回收
const map = new Map();
let obj2 = { id: 2 };
map.set(obj2, 'data');
obj2 = null; // obj2 不会被回收,因为 map 持有引用
// 不一定,取决于闭包引用的内容
function createClosure() {
const largeData = new Array(1000000).fill(0);
return function() {
// 如果闭包引用了 largeData,largeData 不会被回收
console.log(largeData.length);
};
}
// 如果闭包没有引用外部变量,不会有问题
function createClosure2() {
const largeData = new Array(1000000).fill(0);
return function() {
// 没有引用 largeData,largeData 可以被回收
console.log('hello');
};
}
class ResourceManager {
constructor() {
this.timers = [];
this.listeners = [];
}
addTimer(callback, delay) {
const id = setInterval(callback, delay);
this.timers.push(id);
return id;
}
addListener(element, event, handler) {
element.addEventListener(event, handler);
this.listeners.push({ element, event, handler });
}
cleanup() {
this.timers.forEach(id => clearInterval(id));
this.listeners.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});
this.timers = [];
this.listeners = [];
}
}
// 不好的做法
window.largeData = new Array(1000000).fill(0);
// 好的做法:使用模块作用域或函数作用域
(function() {
const largeData = new Array(1000000).fill(0);
// 使用 largeData
})();
// 不好的做法
document.querySelectorAll('button').forEach(button => {
button.addEventListener('click', handler);
});
// 好的做法:事件委托
document.addEventListener('click', function(e) {
if (e.target.tagName === 'BUTTON') {
handler(e);
}
});
// setInterval 可能在某些情况下导致内存问题
// requestAnimationFrame 更适合动画
function animate() {
// 动画逻辑
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);