014-垃圾回收机制详解

JavaScript 垃圾回收机制详解

一、内存管理基础

内存生命周期

  1. 分配:创建变量、对象或函数时分配内存
  2. 使用:程序读写内存的过程
  3. 释放:不再需要时回收内存空间

JavaScript 使用自动内存管理,开发者无需手动分配/释放内存,由垃圾回收器(Garbage Collector)自动完成。

二、垃圾回收核心算法

1. 引用计数(Reference Counting) - 早期方案

  • 原理:跟踪每个值的引用次数
  • 回收条件:引用数为 0 时立即回收
  • 致命缺陷:无法处理循环引用
1
2
3
4
5
6
7
8
9
10
11
12
13
// 循环引用示例
function createCycle() {
let objA = {};
let objB = {};

objA.ref = objB; // objA 引用 objB
objB.ref = objA; // objB 引用 objA

return '循环已创建';
}
createCycle();
// 函数执行后,objA 和 objB 互相引用,引用计数不为 0
// 但实际已无法访问,应被回收但无法被引用计数算法识别

2. 标记-清除(Mark-and-Sweep) - 现代主流方案

  • 原理:从根对象(全局对象)出发,标记所有可达对象,清除未标记对象
  • 执行阶段
    1. 标记阶段:从根对象开始遍历并标记所有可达对象
    2. 清除阶段:回收未被标记的内存块
1
2
3
4
5
6
7
8
graph TD
A[垃圾回收启动] --> B[标记阶段]
B --> C[从根对象开始遍历]
C --> D[标记所有可达对象]
D --> E[清除阶段]
E --> F[扫描整个堆]
F --> G[释放未标记内存]
G --> H[整理内存碎片 - 可选]

三、V8 引擎的垃圾回收实现

内存分代管理

V8 将堆内存分为两个区域:

区域 大小 对象特征 GC算法 执行频率
新生代 1-8MB 存活时间短 Scavenge 高频率
老生代 较大 存活时间长 标记-清除/标记-整理 低频率

新生代回收:Scavenge 算法

  • 内存布局:分为两个等大的 semi-space(From 和 To)
  • 回收过程
    1. 从根对象出发,标记活跃对象
    2. 将活跃对象复制到 To 空间
    3. 清空 From 空间
    4. 交换 From 和 To 空间角色
  • 晋升机制:经历两次 GC 仍存活的对象移动到老生代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 新生代 GC 过程示意
function newGenerationGC() {
// 初始状态:From 空间有对象 A,B,C
let From = [A, B, C];
let To = [];

// 标记阶段:假设 B 不可达
let liveObjects = [A, C];

// 复制阶段
To = [...liveObjects]; // [A, C]

// 清空 From
From = [];

// 空间交换
[From, To] = [To, From]; // 现在 From = [A, C], To = []
}

老生代回收:标记-清除与标记-整理

  1. 标记阶段:深度优先遍历标记活跃对象
  2. 清除阶段:回收死亡对象内存
  3. 内存整理(可选):解决内存碎片问题

优化策略

  1. 增量标记(Incremental Marking)

    • 将标记过程分解为多个小步骤
    • 在 JS 执行间隙进行,减少停顿时间
  2. 惰性清理(Lazy Sweeping)

    • 延迟清理过程,在程序空闲时执行
    • 与增量标记配合减少卡顿
  3. 并发标记/清理

    • 在后台线程并行执行 GC 任务
    • 完全不阻塞主线程

四、常见内存泄漏场景与防范

1. 意外全局变量

1
2
3
4
function leak() {
leakedVar = '这个变量会泄漏到全局'; // 缺少 var/let/const
this.globalVar = '方法中的this指向全局';
}

防范:使用严格模式 'use strict'

2. 未清理的定时器与回调

1
2
3
4
5
6
const intervalId = setInterval(() => {
// 即使不再需要,定时器仍保持回调引用
}, 1000);

// 需要时清除
// clearInterval(intervalId);

3. DOM 引用未释放

1
2
3
4
5
6
7
8
9
10
const elements = {
button: document.getElementById('myButton'),
image: document.getElementById('myImage')
};

// 即使从DOM移除,JS引用仍存在
document.body.removeChild(elements.button);

// 解决方案:移除后置空引用
elements.button = null;

4. 闭包保留不必要引用

1
2
3
4
5
6
7
8
9
10
11
12
function createClosure() {
const largeData = new Array(1000000).fill('*'); // 大数组

return () => {
console.log('闭包执行');
// 实际未使用largeData,但仍被保留
};
}

// 优化:需要时显式释放
let closure = createClosure();
closure = null; // 释放闭包引用

五、内存分析工具

Chrome DevTools

  1. Memory 面板

    • Heap Snapshot:堆内存快照分析
    • Allocation Timeline:内存分配时间线
    • Allocation Sampling:内存分配采样
  2. Performance 面板

    • 记录内存使用变化趋势
    • 识别周期性内存泄漏

Node.js 内存分析

  1. process.memoryUsage()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    console.log(process.memoryUsage());
    /* 输出:
    {
    rss: 24.5MB, // 常驻内存
    heapTotal: 6.2MB, // 堆总量
    heapUsed: 4.3MB, // 已使用堆
    external: 0.8MB // V8管理的C++对象内存
    }
    */
  2. --inspect 标志:

    1
    node --inspect app.js

    连接 Chrome DevTools 进行内存分析

六、最佳实践

  1. 避免内存泄漏

    • 及时清除定时器/事件监听器
    • 解除不必要的对象引用
    • 使用弱引用(WeakMap/WeakSet)
  2. 优化对象创建

    • 复用对象而不是频繁创建新对象
    • 避免在循环中创建函数
  3. 内存敏感操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 处理大数组时使用流式处理
    function processLargeArray(array) {
    for (let i = 0; i < array.length; i++) {
    // 逐项处理而非创建新数组
    }
    }

    // 使用对象池复用对象
    const pool = {
    acquire() { /* 获取对象 */ },
    release(obj) { /* 放回对象池 */ }
    };
  4. 监控内存使用

    1
    2
    3
    4
    5
    6
    7
    8
    // 定期检查内存
    setInterval(() => {
    const mem = process.memoryUsage();
    if (mem.heapUsed > 100 * 1024 * 1024) {
    // 超过100MB时告警
    console.warn('内存使用过高!');
    }
    }, 5000);

七、特殊数据结构

弱引用(Weak Reference)

  • WeakMap:键为弱引用(必须是对象)
  • WeakSet:成员为弱引用
  • 特点:不阻止垃圾回收,不暴露可遍历接口
1
2
3
4
5
const weakMap = new WeakMap();
let obj = { data: '重要信息' };

weakMap.set(obj, '关联元数据');
obj = null; // 下次GC时会自动清除weakMap中的条目

总结

JavaScript 垃圾回收机制是现代浏览器的核心技术之一。理解其工作原理(特别是分代回收和标记-清除算法)对于编写高性能、低内存消耗的应用至关重要。通过合理使用内存分析工具、遵循最佳实践并警惕常见内存泄漏模式,开发者可以显著提升应用的内存使用效率。