JavaScript 使用自动垃圾回收(Garbage Collection)机制来管理内存,释放不再使用的对象所占用的内存。
理解垃圾回收机制有助于:
垃圾是指程序不再需要的内存数据。JavaScript 中主要是不再被引用的对象。
// 示例:创建垃圾
let obj = { name: "John" }; // 对象被引用
obj = null; // 对象不再被引用 → 变为垃圾
垃圾回收是周期性进行的,时机由引擎决定:
工作原理:跟踪每个值的引用次数,当引用数为 0 时回收。
let a = { x: 1 }; // 对象 {x:1} 引用计数 = 1
let b = a; // 对象 {x:1} 引用计数 = 2
a = null; // 引用计数 = 1
b = null; // 引用计数 = 0 → 可回收
致命缺陷:循环引用
function problem() {
let obj1 = {};
let obj2 = {};
obj1.ref = obj2; // obj1 引用 obj2
obj2.ref = obj1; // obj2 引用 obj1
// 函数执行完毕,但引用计数不为 0 → 内存泄漏
// obj1.refCount = 1 (被 obj2.ref 引用)
// obj2.refCount = 1 (被 obj1.ref 引用)
}
problem(); // 内存泄漏!
IE6-7 的问题
// DOM 与 JS 对象的循环引用
let element = document.getElementById("myDiv");
let obj = {};
obj.element = element; // JS 对象引用 DOM
element.someObj = obj; // DOM 引用 JS 对象
// 即使移除 DOM,内存也不会释放
工作原理(两个阶段):
阶段 1:标记(Mark)
从根对象(Roots)出发,标记所有可达对象。
// 根对象包括:
// 1. 全局变量(window/global)
// 2. 当前执行上下文中的局部变量
// 3. 活动函数中的参数和变量
阶段 2:清除(Sweep)
遍历堆内存,回收未被标记的对象。
function createObjects() {
let obj1 = { a: 1 }; // 可达
let obj2 = { b: 2 }; // 可达
obj1.ref = obj2; // obj1 引用 obj2
let obj3 = { c: 3 }; // 可达
let obj4 = { d: 4 }; // 可达
obj3.ref = obj4;
obj4.ref = obj3; // 循环引用
// 函数结束
// obj1, obj2 不可达(局部变量)
// obj3, obj4 不可达(局部变量)
}
createObjects();
// 标记阶段:从根出发,找不到这些对象
// 清除阶段:全部回收(包括循环引用的 obj3, obj4)
标记-清除解决循环引用
let objA = { name: "A" };
let objB = { name: "B" };
objA.ref = objB;
objB.ref = objA; // 循环引用
// 手动断开
objA = null;
objB = null;
// 标记-清除算法:
// 1. 从根对象出发,找不到 objA 和 objB
// 2. 标记阶段:都不标记
// 3. 清除阶段:全部回收
V8 采用分代垃圾回收策略,将内存分为不同代(Generations)。
堆内存结构:
┌─────────────────────────────────────────┐
│ 新生代(New Space) │
│ ┌─────────────┬─────────────┐ │
│ │ From空间 │ To空间 │ │
│ └─────────────┴─────────────┘ │
│ (大小约 1-8MB,Scavenger 算法) │
├─────────────────────────────────────────┤
│ 老生代(Old Space) │
│ (大对象、晋升对象,Mark-Sweep-Compact)│
├─────────────────────────────────────────┤
│ 大对象空间(Large Object Space)│
│ (>1MB 对象,单独分配) │
├─────────────────────────────────────────┤
│ 代码空间(Code Space) │
│ (JIT 编译的代码) │
├─────────────────────────────────────────┤
│ Map空间(Map Space) │
│ (隐藏类和对象描述符) │
└─────────────────────────────────────────┘
特点:
Scavenge 过程(Cheney 算法):
初始状态:
From空间:[活跃对象][垃圾对象][活跃对象]
To空间:空
步骤1:标记 From 空间的活跃对象
步骤2:复制活跃对象到 To 空间
To空间:[活跃对象][活跃对象]
步骤3:交换 From 和 To
From空间:[活跃对象][活跃对象]
To空间:空(准备下次回收)
对象晋升(Promotion)
满足以下条件之一,对象会晋升到老生代:
function createObjects() {
// 新生代分配
let temp = { x: 1 }; // 新生代
// 长期持有 → 晋升到老生代
window.globalObj = { y: 2 }; // 会被晋升
// 大对象直接进入老生代
let largeArray = new Array(1000000); // 老生代
}
特点:
回收过程:
// 老生代回收触发条件:
// 1. 老生代空间不足
// 2. 新生代晋升失败
// 3. 手动触发
// 主要算法:
// 1. 增量标记(Incremental Marking)- 减少停顿
// 2. 标记-清除(Mark-Sweep)
// 3. 标记-整理(Mark-Compact)- 减少碎片
标记-整理(Mark-Compact):
整理前: [活跃][垃圾][活跃][垃圾][活跃]
标记后: [活跃] [活跃] [活跃]
整理后: [活跃][活跃][活跃]
问题:全停顿(Stop-The-World)
// GC 期间,JS 执行暂停
function criticalTask() {
// 如果此时触发 GC,用户会感到卡顿
updateUI();
processData();
// GC 停顿可能几百毫秒
}
解决方案:增量标记(Incremental Marking)
将标记过程分成多个小步骤,与 JS 执行交替进行。
// 传统:标记阶段完全阻塞
// [标记全部对象] → [执行JS] → [清除]
// 增量:标记与执行交错
// [标记一部分] → [执行JS] → [标记一部分] → [执行JS] → [清除]
三色标记法(Tri-color Marking)
初始:所有对象为白色
步骤:从根开始,标记为灰色
循环:
1. 从灰色集合取一个对象
2. 将其引用对象标记为灰色
3. 自身标记为黑色
结束:灰色集合为空,回收白色对象
function leak1() {
leaked = "I'm global!"; // 没有 var/let/const
this.leaked2 = "Also global"; // 严格模式下安全
}
// 解决方法:
("use strict"); // 严格模式禁止意外全局变量
// 泄漏示例
let data = getHugeData();
setInterval(() => {
let node = document.getElementById('node');
if (node) {
node.innerHTML = JSON.stringify(data); // 持有 data 引用
}
}, 1000);
// 清理
let timerId = setInterval(...);
clearInterval(timerId); // 不再需要时清除
// 泄漏:JS 引用 DOM,DOM 移除后引用仍在
const elements = {
button: document.getElementById("myButton"),
image: document.getElementById("myImage"),
};
// 移除 DOM 但 JS 仍引用
document.body.removeChild(document.getElementById("myButton"));
// elements.button 仍引用已移除的 DOM 节点
// 解决方案:置为 null
elements.button = null;
// 不必要的闭包保持引用
function outer() {
let largeData = new Array(1000000).fill("*");
return function inner() {
// 即使不使用 largeData,闭包也持有引用
return "Hello";
};
}
// 优化:需要时才引用
function outer() {
return function inner() {
let largeData = new Array(1000000).fill("*");
// 使用后释放
return process(largeData);
};
}
// 泄漏:添加后未移除
class Component {
constructor(element) {
this.element = element;
this.handleClick = this.handleClick.bind(this);
element.addEventListener("click", this.handleClick);
}
handleClick() {
/* ... */
}
// 忘记移除监听器
// remove() {
// this.element.removeEventListener('click', this.handleClick);
// }
}
// 控制台日志保持对象引用
let bigData = fetchHugeData();
console.log(bigData); // 控制台保持引用,直到清空
// 生产环境应避免:
if (process.env.NODE_ENV !== "production") {
console.log(bigData);
}
// 步骤:
// 1. 打开 Performance 面板
// 2. 录制操作,观察内存曲线
// 3. 使用 Memory 面板拍快照
// 4. 比较快照,查找未释放对象
// 操作前:拍快照1
// 执行可疑操作
// 操作后:拍快照2
// 比较:关注 #Delta(差异)
// 实时监控
setInterval(() => {
const memory = performance.memory;
console.log(`Used: ${memory.usedJSHeapSize}`);
console.log(`Total: ${memory.totalJSHeapSize}`);
console.log(`Limit: ${memory.jsHeapSizeLimit}`);
}, 5000);
// 使用后置为 null
function processData(data) {
const result = heavyCalculation(data);
data = null; // 帮助 GC
return result;
}
// 强引用:阻止回收
const map = new Map();
let obj = { data: "large" };
map.set("key", obj); // 强引用
obj = null; // map 仍持有引用,obj 不会被回收
// 弱引用:不阻止回收
const weakMap = new WeakMap();
let obj2 = { data: "large" };
weakMap.set(obj2, "metadata"); // 弱引用
obj2 = null; // obj2 可被回收,weakMap 自动删除
// 不好:频繁创建/销毁
function render() {
for (let i = 0; i < 1000; i++) {
let temp = new Array(1000); // 频繁分配
// 使用 temp...
}
}
// 好:复用对象
let pool = [];
function getFromPool() {
return pool.pop() || new Array(1000);
}
function returnToPool(arr) {
arr.length = 0; // 清空但不释放
pool.push(arr);
}
// 不好:一次性处理
function processAll(data) {
// 处理 100万条数据 → 内存峰值高
}
// 好:分块处理
async function processInChunks(data, chunkSize = 1000) {
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
await processChunk(chunk);
await new Promise(resolve => setTimeout(resolve, 0)); // 让 GC 有机会工作
}
}
class ObjectPool {
constructor(createFn) {
this.createFn = createFn;
this.pool = [];
}
acquire() {
return this.pool.pop() || this.createFn();
}
release(obj) {
// 重置对象状态
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
delete obj[key];
}
}
this.pool.push(obj);
}
}
// 使用
const pool = new ObjectPool(() => ({}));
const obj1 = pool.acquire();
// 使用 obj1...
pool.release(obj1); // 放回池中
// 非标准方法,主要用于调试
if (typeof gc === "function") {
gc(); // V8 的 --expose-gc 标志
}
// 模拟垃圾回收触发
function triggerGC() {
const max = 1000000;
for (let i = 0; i < max; i++) {
let obj = {};
obj.self = obj; // 创建后立即丢弃
}
}
# 启动时设置
node --max-old-space-size=4096 app.js # 4GB 老生代
// 不要一次性读取大文件
const fs = require("fs");
const readStream = fs.createReadStream("largefile.txt");
readStream.pipe(process); // 流式处理
const v8 = require("v8");
setInterval(() => {
const heap = process.memoryUsage();
const heapStats = v8.getHeapStatistics();
console.log("Heap used:", heap.heapUsed);
console.log("Heap total:", heap.heapTotal);
console.log("External:", heap.external); // Buffer, Socket 等
}, 10000);
// 问题:为什么现代浏览器不用引用计数?
// 答案:无法处理循环引用
// 问题:闭包一定会内存泄漏吗?
function createClosure() {
let data = new Array(1000000);
return function () {
// 不使用 data,但闭包引用
return "hello";
};
}
// 答案:不一定会泄漏,但可能延长 data 的生命周期
// 问题:什么时候用 WeakMap?
// 答案:存储对象的元数据,不阻止对象被回收
const metadata = new WeakMap();
let obj = { id: 1 };
metadata.set(obj, { created: Date.now() });
obj = null; // metadata 中的条目自动删除