js 是一种高级语言,内存分配和释放由引擎自动完成。垃圾回收(Garbage Collection, GC)作为引擎的一部分,负责释放不再使用的内存。
什么是垃圾回收?
垃圾回收是为了释放不再使用的内存,从而避免内存泄漏和性能下降。js 的垃圾回收主要通过检测对象是否仍然可达(reachable)来确定是否可以回收。
标记-清除(Mark-and-Sweep)
- 从“根对象”(如全局对象和当前作用域的变量)开始,递归检查所有被引用的对象。
- 没有被引用的对象会被清除。
- 优势:解决了循环引用问题。
let globalVar = {};
function foo() {
let localVar = {}; // localVar 在函数结束后变得不可达
}
foo();
引用计数
- 每个对象记录引用它的次数。
- 引用次数为零时,对象被垃圾回收器回收。
- 缺点:无法处理循环引用。
const obj1 = {};
const obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1; // 循环引用,引用计数永不为零
垃圾回收的触发条件
引擎会根据分代模型和内存使用情况,动态调整垃圾回收频率。
分配新内存时,可能触发 GC。
内存压力过大时,强制触发 GC。
V8 引擎中的垃圾回收实现
V8 的分代回收模型
- 新生代对象 (Young Generation)
- 存储生命周期较短的对象(如函数内部变量)。
- 使用 Scavenge 算法:将活跃对象复制到新的内存空间,释放旧内存空间。
let temp = { x: 1 }; // temp 在函数外无用,会快速被回收
- 老生代对象 (Old Generation)
- 存储生命周期较长的对象(如全局变量和缓存)。
- 使用 标记清除 + 标记整理:
- 标记清除 (Mark-and-Sweep):回收不可达对象。
- 标记整理 (Mark-and-Compact):将存活对象压缩到内存的一端,避免碎片化。
增量式和并发式垃圾回收
- 增量式垃圾回收
- 将标记阶段拆分成多个小任务,以减少单次 GC 的暂停时间。
- 并发式垃圾回收
- 标记和回收在主线程与后台线程同时进行,提升性能。
示例代码与生命周期分析
// 新生代对象分配
let shortLived = { data: "temporary" };
// 触发晋升:老生代对象
let longLived = { data: "persistent" };
// longLived 被多次存活,会被移到老生代
垃圾回收中常见问题与示例
1. 意外的全局变量
问题:未正确声明变量会创建全局变量,占用全局作用域,导致内存泄漏。
function leak() {
leakedVar = "I am a leak!"; // 漏洞:`leakedVar` 被自动添加到全局对象
}
leak();
console.log(window.leakedVar); // 全局变量泄漏
解决方法: 严格模式可以防止意外创建全局变量:
"use strict";
function noLeak() {
let localVar = "I am safe!";
}
未清理的定时器和回调
问题:如果未清理定时器或事件监听器,相关对象会被引用,无法被回收。
const button = document.getElementById("btn");
button.addEventListener("click", () => {
console.log("Clicked!");
});
// 如果不移除监听器,即使按钮被移除,回调仍然占用内存
解决方法: 手动清理事件监听器或定时器:
button.removeEventListener("click", callback);
clearInterval(timerId);
3. 闭包的内存占用
问题:闭包会保留外部作用域的变量,可能导致意外的内存泄漏。
function createClosure() {
let data = new Array(1000000).fill("leak");
return function () {
console.log(data.length);
};
}
const closure = createClosure();
// 即使不再需要 `data`,它仍然被闭包引用
解决方法: 确保在闭包中只保留必要的数据:
function createOptimizedClosure() {
let data = new Array(1000000).fill("leak");
return function () {
console.log(data.length);
data = null; // 手动释放引用
};
}
4. DOM 节点的未清理引用
问题:移除 DOM 节点时,如果仍然保留对节点的引用,会导致无法回收。
let element = document.getElementById("myDiv");
document.body.removeChild(element);
// `element` 仍然保留对 DOM 的引用,未被释放
解决方法: 将变量设为 null
来手动清理引用:
element = null;
5. 大量缓存数据
问题:使用全局变量或单例模式时,大量保存数据可能导致内存占用过多。
const cache = {};
for (let i = 0; i < 1000000; i++) {
cache[i] = `Data ${i}`;
}
解决方法:
- 使用弱引用数据结构(如
WeakMap
或WeakSet
)
const weakCache = new WeakMap();
let obj = {};
weakCache.set(obj, "value");
obj = null; // 自动回收
- 定期清理无用数据。
调试工具与方法
- Chrome DevTools
- Memory 面板:捕获堆快照,分析未回收对象。
- Performance 面板:查看 GC 的频率与耗时。
- Node.js
- 使用
--trace-gc
查看垃圾回收日志:
- 使用
node --trace-gc app.js
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
暂无评论内容