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

001-MVC与MVVM架构对比

MVC 与 MVVM 架构对比

MVC(后端概念)

  • **M (Model)**:数据库中的数据
  • **V (View)**:视图层(用户界面)
  • **C (Controller)**:控制器(处理业务逻辑,协调 Model 和 View)

MVVM(前端概念)

  • **M (Model)**:数据层(通过 Ajax 等获取的数据,存储在 data 中)
  • **V (View)**:视图层(HTML 结构)
  • **VM (ViewModel)**:
    • new Vue() 创建的实例对象
    • 作为 View 和 Model 的调度者
    • 实现数据的双向绑定

MVVM

MVVM 核心思想

Model-View-ViewModel 是一种设计思想:

  1. Model:数据模型(含数据操作逻辑)
  2. View:UI 组件(将数据渲染为界面)
  3. ViewModel
    • 同步 View 和 Model 的中间层
    • 通过双向数据绑定自动连接 View 和 Model

工作特点:

✅ View 和 Model 无直接交互,需通过 ViewModel
✅ 数据变化自动同步:

  • Model 变化 → 立即反应到 View
  • View 变化 → 自动同步到 Model
    无需手动操作 DOM,开发者专注业务逻辑

MVC vs MVVM 区别

特性 MVC MVVM
数据同步 需手动更新 View 自动双向绑定
DOM 操作 频繁操作 DOM 零 DOM 操作
性能影响 易引起性能下降 高效渲染
核心组件 Controller 处理逻辑 ViewModel 管理数据绑定

MVVM vs jQuery 区别

维度 jQuery MVVM (如 Vue)
开发模式 命令式 DOM 操作 数据驱动视图
数据管理 手动维护状态 自动状态同步
代码组织 易产生冗余代码 组件化+响应式数据流

适用场景

  1. 适合 MVVM 的场景

    • 数据操作复杂的应用(如表单、实时仪表盘)
    • 需要高效视图更新的 SPA(单页应用)
    • 追求开发效率的中大型项目
  2. 适合 jQuery 的场景

    • 轻度交互的静态页面
    • 需快速实现简单动画/效果
    • 遗留系统维护

核心总结
MVVM 通过数据绑定解决了 MVC 的 DOM 性能瓶颈,适用于数据驱动型应用;
MVC 的 Controller 在 MVVM 中进化为 ViewModel,实现关注点分离。