文章目录

JavaScript 内存管理:垃圾回收与内存泄漏

发布于 2026-04-17 21:25:20 · 浏览 216 次 · 评论 0 条

JavaScript 内存管理:垃圾回收与内存泄漏

JavaScript 具备自动内存管理机制,开发者无需像 C 或 C++ 那样手动分配(malloc)和释放(free)内存。然而,理解其背后的垃圾回收(Garbage Collection, 简称 GC)机制,是编写高性能代码和避免内存泄漏的关键。


理解垃圾回收机制

JavaScript 引擎中最核心的垃圾回收算法是“标记-清除”。该算法主要解决的核心问题是:如何判断一个对象不再被需要?

标记-清除算法原理

该算法将“对象是否不再需要”简化定义为“对象是否不可达”。

  1. 识别根节点:垃圾回收器从一组被称为“根”的全局变量开始(如 window 对象、当前执行栈的变量)。
  2. 标记活跃对象:从根节点开始,遍历所有引用。记录所有能访问到的对象为“活跃”。
  3. 清除垃圾:遍历完成后,内存中未被标记的对象被视为“垃圾”。
  4. 释放内存:回收这些垃圾对象占用的内存空间。

以下是标记过程的逻辑流程:

graph TD A["开始: GC 触发"] --> B["步骤: 标记根节点"] B --> C{检查: 引用是否可达?} C -- 是 --> D["动作: 标记为活跃"] C -- 否 --> E["状态: 保持未标记"] D --> F{遍历: 还有引用?} F -- 是 --> C F -- 否 --> G["步骤: 执行清除"] E --> G G --> H["结束: 内存释放"]

V8 引擎的优化策略:分代回收

为了提高性能,V8 引擎(Chrome 和 Node.js 的核心)基于“分代假说”(大部分对象存活时间短,少部分对象存活时间长),将堆内存分为两个区域:新生代 和 老生代。

区域 存储对象特征 使用算法 特点
新生代 存活时间短(如临时变量) Scavenge 算法 空间小,回收频率高
老生代 存活时间长(如全局配置、闭包) 标记-清除 + 标记-整理 空间大,回收频率低

Scavenge 算法(新生代)

该算法采用 Cheney 算法,将内存分为两块:From 空间和 To 空间。

  1. 分配:新对象存入 From 空间。
  2. 复制:GC 开始时,检查 From 空间中的存活对象。
  3. 移动:将存活对象复制到 To 空间,并整理内存(消除碎片)。
  4. 交换:清空 From 空间,交换 From 和 To 的角色。

常见内存泄漏与解决方案

内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。

1. 意外的全局变量

在函数内部声明变量时,如果遗漏关键字,它会挂载到全局对象上,导致无法被回收。

问题代码:

function leak() {
    foo = "I am global now"; 
}

修复步骤:

  1. 添加 const、let 或 var 关键字。
  2. 重写为 const foo = "I am global now";。

2. 未清除的定时器或回调

setInterval 或 setTimeout 只要没有被清除,其回调函数引用的所有对象都不会被回收。

问题代码:

function startTimer() {
    setInterval(() => {
        // 这里的代码如果在对象销毁后还在运行,就会导致泄漏
        const data = hugeData;
        console.log(data);
    }, 1000);
}

修复步骤:

  1. 保存定时器的 ID:const timerId = setInterval(...)。
  2. 在组件销毁或不再需要时,调用 clearInterval(timerId)。

3. 闭包

闭包会维持对外部函数作用域的引用。如果闭包被意外长期持有,作用域内的变量就无法释放。

问题代码:

const getHandler = () => {
    const hugeObject = new Array(1000000);
    return () => {
        // 即使这里没有使用 hugeObject,闭包依然保留了作用域引用
        console.log("Hello");
    };
};

// handler 一直存在,hugeObject 就无法被回收
const handler = getHandler();

修复步骤:

  1. 在不再使用 handler 时,将其赋值为 null。
  2. 执行 handler = null;。

4. 脱离 DOM 的引用

有时虽然 DOM 节点已被移除,但 JavaScript 对象中仍保留着对它的引用。

问题代码:

let btn = document.getElementById('button');
document.body.removeChild(btn); // DOM 中已删除

// 但 btn 变量依然引用着这个对象,内存无法回收

修复步骤:

  1. 在移除 DOM 节点后,立即清除引用。
  2. 执行 btn = null;。

如何检测内存泄漏

使用 Chrome DevTools 的 Memory 面板可以直观地发现内存问题。

  1. 打开 Chrome 浏览器,按下 F12 打开 开发者工具。
  2. 点击 顶部的 Memory 标签页。
  3. 选择 Heap snapshot 选项。
  4. 点击 Take snapshot 按钮拍摄初始快照。

执行测试操作:在页面上进行一系列可能导致内存泄漏的操作(如反复打开和关闭弹窗)。

  1. 再次点击 Take snapshot 拍摄第二个快照。
  2. 选择 第二个快照,切换 视图模式为 Comparison(对比)。
  3. 查看 # Delta 列。
    • 如果发现对象数量 # New 在增加且 # Deleted 很少,说明存在内存泄漏。
  4. 展开 有疑问的对象,查看 Retainers 面板,追踪 是哪里保留了引用。

另一种方法是使用 Allocation instrumentation on timeline:

  1. 选择 Allocation instrumentation on timeline。
  2. 点击 Start 按钮。
  3. 操作 页面,模拟用户行为。
  4. 点击 Stop 按钮。
  5. 观察 时间轴上的蓝色柱状图。
    • 如果出现不断升高且不回落的阶梯状线条,说明内存持续增长,存在泄漏。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文