{T}

内存管理

JavaScript 使用垃圾回收机制自动管理内存,但开发者仍需注意内存泄漏问题。理解内存管理原理有助于编写高性能代码。

一、JavaScript 内存模型

1. 内存结构

code
┌─────────────────────────────────────────────────────┐
│                   V8 内存结构                        │
├─────────────────────────────────────────────────────┤
│  ┌─────────────────┐  ┌─────────────────────────┐  │
│  │   Stack 栈内存   │  │    Heap 堆内存          │  │
│  │                 │  │  ┌───────────────────┐  │  │
│  │  - 原始类型     │  │  │   New Space       │  │  │
│  │  - 引用地址     │  │  │   (新生代 1-8MB)   │  │  │
│  │  - 执行上下文   │  │  └───────────────────┘  │  │
│  │                 │  │  ┌───────────────────┐  │  │
│  └─────────────────┘  │  │   Old Space       │  │  │
│                       │  │   (老生代)         │  │  │
│                       │  └───────────────────┘  │  │
│                       └─────────────────────────┘  │
└─────────────────────────────────────────────────────┘

2. 栈内存与堆内存

类型栈内存 (Stack)堆内存 (Heap)
存储内容原始类型、引用地址对象、数组、函数
大小限制固定大小动态分配
分配速度较慢
生命周期函数调用结束释放垃圾回收释放
线程共享线程独立线程共享
javascript
// 栈内存 - 原始类型
let num = 123;        // 直接存储值
let str = 'hello';    // 直接存储值
let bool = true;      // 直接存储值

// 堆内存 - 引用类型
const obj = { a: 1 }; // 栈存引用,堆存对象
const arr = [1, 2];   // 栈存引用,堆存数组

3. 新生代与老生代

code
对象生命周期:

  新生代 (New Space)
  ┌─────────────────┐
  │  新创建的对象    │ ← 短生命周期对象
  │  Scavenge 算法  │
  └────────┬────────┘
           │ 存活足够久
           ▼
  老生代 (Old Space)
  ┌─────────────────┐
  │  长生命周期对象  │ ← 标记清除算法
  │  Mark-Sweep     │
  └─────────────────┘

二、垃圾回收机制

1. 标记清除算法

code
标记清除过程:

Step 1: 标记阶段
┌─────────────────────────────────┐
│  Root → A → B → C               │
│         ↓                       │
│         D ← E (不可达)          │
│                                 │
│  ✓ A, B, C, D: 可达(标记)      │
│  ✗ E: 不可达(未标记)           │
└─────────────────────────────────┘

Step 2: 清除阶段
┌─────────────────────────────────┐
│  Root → A → B → C               │
│         ↓                       │
│         D     [回收 E]          │
│                                 │
│  清除未标记的对象                │
└─────────────────────────────────┘
javascript
// 垃圾回收示例
function createObjects() {
  const obj1 = { name: 'obj1' }; // 创建对象
  const obj2 = { name: 'obj2' }; // 创建对象

  return obj1; // obj1 可达,obj2 在函数结束后不可达
}

const result = createObjects();
// obj2 被垃圾回收

2. 引用计数算法

javascript
// 引用计数的问题:循环引用

// ❌ 循环引用导致内存泄漏(旧版 IE)
function createCircular() {
  const obj1 = {};
  const obj2 = {};

  obj1.ref = obj2; // obj2 引用计数 +1
  obj2.ref = obj1; // obj1 引用计数 +1

  // 函数结束后,obj1 和 obj2 的引用计数都为 1
  // 无法被回收
}

// ✅ 现代浏览器使用标记清除,可处理循环引用
// 但仍需注意闭包等特殊情况

3. 分代回收

javascript
// V8 分代回收策略

// 新生代:Scavenge 算法(复制算法)
// - 将内存分为两半:From 空间和 To 空间
// - 存活对象从 From 复制到 To
// - 清空 From,交换 From 和 To

// 老生代:标记清除 + 标记整理
// - 标记清除:标记存活对象,清除未标记对象
// - 标记整理:移动存活对象,消除内存碎片

4. 垃圾回收触发条件

条件说明
内存不足堆内存接近上限
定期回收空闲时自动执行
手动触发global.gc()(需启用)

三、常见内存泄漏

1. 全局变量

javascript
// ❌ 意外的全局变量
function leak() {
  bar = 'This is a global variable'; // 未声明,成为全局变量
}

// ✅ 使用严格模式
'use strict';
function noLeak() {
  const bar = 'local variable';
}

// ✅ 使用模块作用域
const moduleScope = (() => {
  let privateVar = 'private';
  return {
    getVar: () => privateVar
  };
})();

2. 闭包

javascript
// ❌ 闭包持有大对象
function createClosure() {
  const largeData = new Array(1000000).fill('x');

  return function() {
    console.log(largeData.length); // largeData 无法被回收
  };
}

// ✅ 释放不需要的数据
function createClosure() {
  const largeData = new Array(1000000).fill('x');
  const length = largeData.length; // 只保留需要的值

  return function() {
    console.log(length);
  };
  // largeData 在函数结束后可被回收
}

// ✅ 使用 let 和块级作用域
function createClosures() {
  const closures = [];

  for (let i = 0; i < 1000; i++) { // 使用 let
    closures.push(() => i); // 每个闭包捕获独立的 i
  }

  return closures;
}

3. 事件监听

javascript
// ❌ 未清理事件监听
class Component {
  mount() {
    window.addEventListener('resize', this.handleResize);
  }
}

// ✅ 组件销毁时清理
class Component {
  constructor() {
    this.handleResize = this.handleResize.bind(this);
  }

  // 在挂载时监听
  mount() {
    this.controller = new AbortController();
    window.addEventListener('resize', this.handleResize, {
      signal: this.controller.signal,
    });
  }

  unmount() {
    this.controller.abort(); // 自动移除所有监听器
  }
}

4. 定时器

javascript
// ❌ 未清理定时器
function startTimer() {
  setInterval(() => {
    // 持续运行,即使组件已销毁
  }, 1000);
}

// ✅ 清理定时器
class Timer {
  start() {
    this.timerId = setInterval(() => {}, 1000);
  }

  stop() {
    clearInterval(this.timerId);
  }
}

// ✅ 返回清理函数
function startTimer(callback, delay) {
  const timerId = setInterval(callback, delay);
  return () => clearInterval(timerId);
}

// 使用
const cleanup = startTimer(() => {}, 1000);
cleanup(); // 停止定时器

5. DOM 引用

javascript
// ❌ 保留已删除 DOM 的引用
const elements = [];
function addElement() {
  const el = document.createElement('div');
  document.body.appendChild(el);
  elements.push(el); // 保存引用
}

function removeElement(el) {
  document.body.removeChild(el);
  // elements 数组仍持有引用,导致内存泄漏
}

// ✅ 移除 DOM 时同时清理引用
function removeElementSafe(el) {
  const index = elements.indexOf(el);
  if (index > -1) {
    elements.splice(index, 1);
  }
  document.body.removeChild(el);
}

6. console.log

javascript
// ❌ console.log 持有对象引用
function debug() {
  const largeObject = { /* 大量数据 */ };
  console.log('Debug:', largeObject);
  // 在开发者工具中,largeObject 不会被回收
}

// ✅ 生产环境移除 console
if (process.env.NODE_ENV === 'production') {
  console.log = () => {};
}

// ✅ 使用条件日志
function debug(message, data) {
  if (DEBUG_MODE) {
    console.log(message, JSON.parse(JSON.stringify(data)));
  }
}

四、内存优化技巧

1. 使用 WeakMap/WeakSet

javascript
// WeakMap 的键是弱引用
const cache = new WeakMap();

function cacheData(obj, data) {
  cache.set(obj, data);
}

// 当 obj 被回收时,cache 中的条目自动清除

// 应用示例:存储 DOM 元素的额外数据
const elementData = new WeakMap();

function bindData(element, data) {
  elementData.set(element, data);
}

function getData(element) {
  return elementData.get(element);
}

// 元素移除后,数据自动释放

2. 及时释放引用

javascript
// ✅ 手动释放大对象
function process() {
  let largeData = new Array(1000000).fill(0);

  // 使用 largeData
  const result = largeData.reduce((a, b) => a + b, 0);

  // 释放引用
  largeData = null;

  return result;
}

// ✅ 清理对象属性
function clearObject(obj) {
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      delete obj[key];
    }
  }
}

// ✅ 使用 Object.create(null) 创建纯净对象
const cleanObj = Object.create(null);
// 没有原型链,更节省内存

3. 对象池模式

javascript
class ObjectPool {
  constructor(factory, initialSize = 10) {
    this.factory = factory;
    this.pool = [];

    // 预创建对象
    for (let i = 0; i < initialSize; i++) {
      this.pool.push(this.factory());
    }
  }

  acquire() {
    // 有可用对象则取出,否则新建
    return this.pool.pop() || this.factory();
  }

  release(obj) {
    this.pool.push(obj);
  }
}

// 使用对象池
const particlePool = new ObjectPool(() => ({ x: 0, y: 0, vx: 0, vy: 0 }));

function createParticle() {
  const particle = particlePool.acquire();
  return particle;
}

function destroyParticle(particle) {
  particlePool.release(particle);
}

4. 避免创建临时对象

javascript
// ❌ 循环中创建新对象
function processItems(items) {
  return items.map(item => {
    const result = { value: item.value }; // 每次创建新对象
    return result;
  });
}

// ✅ 重用对象
function processItems(items) {
  const result = [];
  const temp = { value: null };

  for (const item of items) {
    temp.value = item.value;
    result.push({ ...temp });
  }

  return result;
}

// ✅ 使用 TypedArray 处理数值数据
const floats = new Float64Array(1000);
for (let i = 0; i < 1000; i++) {
  floats[i] = Math.random();
}

5. 字符串优化

javascript
// ❌ 大量字符串拼接
let html = '';
for (let i = 0; i < 10000; i++) {
  html += '<div>' + i + '</div>'; // 每次创建新字符串
}

// ✅ 使用数组 join
const parts = [];
for (let i = 0; i < 10000; i++) {
  parts.push('<div>' + i + '</div>');
}
const html = parts.join('');

// ✅ 使用模板字符串
const items = [1, 2, 3, 4, 5];
const html = items.map(i => `<div>${i}</div>`).join('');

五、内存监控与分析

1. Chrome DevTools

code
Memory 面板功能:

1. Heap Snapshot(堆快照)
   - 查看当前内存中的所有对象
   - 对比不同时间点的快照,找出泄漏

2. Allocation instrumentation on timeline(分配时间线)
   - 记录内存分配过程
   - 蓝色柱子表示内存分配

3. Allocation sampling(分配采样)
   - 低开销的内存分析
   - 查看函数的内存分配情况

2. Performance API

javascript
// 获取内存使用情况
function getMemoryInfo() {
  if (performance.memory) {
    return {
      usedJSHeapSize: performance.memory.usedJSHeapSize,   // 已用堆内存
      totalJSHeapSize: performance.memory.totalJSHeapSize, // 总堆内存
      jsHeapSizeLimit: performance.memory.jsHeapSizeLimit, // 堆内存限制

      // 计算使用率
      usagePercent: (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit * 100).toFixed(2)
    };
  }
  return null;
}

console.log(getMemoryInfo());

3. 内存监控

javascript
class MemoryMonitor {
  constructor(options = {}) {
    this.interval = options.interval || 5000;
    this.warningThreshold = options.warningThreshold || 0.8;
    this.history = [];
    this.timer = null;
  }

  start() {
    this.timer = setInterval(() => {
      const memory = this.getMemory();
      this.history.push({
        time: Date.now(),
        ...memory,
      });

      // 内存使用超过警告阈值时报警
      if (memory.usageRatio > this.warningThreshold) {
        console.warn(
          `内存占用过高: ${(memory.usageRatio * 100).toFixed(1)}%`
        );
      }
    }, this.interval);
  }

  stop() {
    clearInterval(this.timer);
  }

  getMemory() {
    const memory = performance.memory || {};
    const { usedJSHeapSize = 0, jsHeapSizeLimit = 1 } = memory;
    return {
      used: usedJSHeapSize,
      usageRatio: usedJSHeapSize / jsHeapSizeLimit,
    };
  }
}

// 使用
const monitor = new MemoryMonitor({ interval: 10000 });
monitor.start();

4. 检测内存泄漏

javascript
// 堆快照对比法
async function detectMemoryLeak() {
  // 1. 记录初始状态
  const snapshot1 = await takeHeapSnapshot();

  // 2. 执行可能泄漏的操作
  for (let i = 0; i < 100; i++) {
    performOperation();
  }

  // 3. 强制垃圾回收(需要启动时添加 --expose-gc)
  if (global.gc) {
    global.gc();
    const snapshot2 = await takeHeapSnapshot();

    // 4. 对比堆快照,计算内存变化
    const leaked = snapshot2.totalSize - snapshot1.totalSize;
    console.log('内存变化:', (leaked / 1024 / 1024).toFixed(2), 'MB');

    if (leaked > 1024 * 1024) { // 超过 1MB
      console.warn('可能存在内存泄漏');
    }
  }
}

六、最佳实践

1. 内存管理检查清单

  • 使用严格模式避免全局变量
  • 及时清理事件监听器
  • 及时清理定时器
  • 避免闭包捕获不必要的数据
  • 使用 WeakMap 存储对象元数据
  • 大对象使用后置为 null
  • 生产环境移除调试日志
  • 使用对象池重用对象
  • 使用 WeakRef 进行条件性缓存
  • 使用 FinalizationRegistry 监控对象回收

2. 代码模式对比

模式内存影响推荐程度
模块作用域隔离全局变量⭐⭐⭐
WeakMap弱引用存储⭐⭐⭐
WeakRef条件性缓存⭐⭐
对象池重用对象⭐⭐
块级作用域及时释放⭐⭐⭐
闭包缓存谨慎使用⭐⭐

3. WeakRef 与 FinalizationRegistry(ES2021)

WeakRefFinalizationRegistry 是 ES2021 引入的 API,用于更精细的内存管理:

javascript
// WeakRef - 创建对对象的弱引用
let target = { data: new Array(1000000).fill("x") }
const ref = new WeakRef(target)

// 获取引用对象(可能已被回收)
const obj = ref.deref()
if (obj) {
  console.log("对象仍然存在:", obj.data.length)
} else {
  console.log("对象已被垃圾回收")
}

// FinalizationRegistry:对象被回收时执行清理回调
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`资源被回收: ${heldValue}`)
})

let resource = { id: "resource-1", data: new ArrayBuffer(1024 * 1024) }
registry.register(resource, "resource-1")

// 当 resource 被垃圾回收时,回调会被调用
resource = null // 允许垃圾回收

⚠️ WeakRef 使用注意事项 - WeakRef 适用于缓存等可接受对象随时消失的场景

  • 避免在核心逻辑中依赖 WeakRef.deref() 的结果
  • FinalizationRegistry 的回调时机不可预测,不能用于关键资源释放
  • 对于确定性的资源释放,请使用 ES2025 的 using 声明

七、常见问题解答

Q1: 如何判断是否存在内存泄漏?

A: 判断方法:

javascript
// 1. Chrome DevTools Memory 面板
//    - 多次操作后查看堆大小是否持续增长

// 2. Performance Monitor
//    - 打开 DevTools → More tools → Performance Monitor
//    - 观察 JS heap size 曲线

// 3. 代码检测
const monitor = new MemoryMonitor();
monitor.start();

// 执行操作
performAction();

// 查看报告
console.log(monitor.getReport());

Q2: WeakMap 和 Map 有什么区别?

A: 主要区别:

javascript
// Map - 强引用
const map = new Map();
let obj = { name: 'test' };
map.set(obj, 'data');
obj = null; // obj 不会被回收,map 仍持有引用

// WeakMap - 弱引用
const weakMap = new WeakMap();
obj = { name: 'test' };
weakMap.set(obj, 'data');
obj = null; // obj 可以被回收,WeakMap 条目自动清除

// WeakMap 特点:
// 1. 键必须是对象
// 2. 键是弱引用,不阻止垃圾回收
// 3. 不可遍历
// 4. 没有 size 属性

Q3: 如何处理大型数据集?

A: 处理策略:

javascript
// 1. 分页加载
async function loadPage(page, size) {
  const data = await fetch(`/api/data?page=${page}&size=${size}`);
  return data;
}

// 2. 流式处理
async function processStream(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    // 处理数据块
    processChunk(value);
  }
}

// 3. 虚拟滚动
// 只渲染可见区域的数据

// 4. Web Worker
// 将数据处理放到 Worker 中
const worker = new Worker('data-processor.js');
worker.postMessage(largeData);

Q4: 何时需要手动触发垃圾回收?

A: 一般不需要,但在以下场景可以考虑:

javascript
// 仅在 Node.js 环境可用
if (global.gc) {
  // 场景1:大量内存释放后
  largeArray = null;
  global.gc();

  // 场景2:性能测试前
  global.gc();
  const start = performance.now();
  // 测试代码
}

Q5: 如何优化 SPA 应用的内存?

A: SPA 内存优化策略:

javascript
// 1. 路由切换时清理
router.beforeEach((to, from, next) => {
  // 清理上一页的数据
  store.dispatch('clearPageData', from.path);
  next();
});

// 2. 组件销毁时清理
onUnmounted(() => {
  // 取消请求
  controller.abort();
  // 清理事件
  eventBus.off('event', handler);
  // 清理定时器
  clearInterval(timer);
});

// 3. 缓存策略
const cache = new LRUCache({
  max: 100,
  maxAge: 1000 * 60 * 5 // 5 分钟
});

// 4. 懒加载
const LazyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
);

八、V8 垃圾回收算法

V8 引擎采用分代回收策略,针对不同生命周期的对象使用不同的回收算法,以在回收效率和性能开销之间取得平衡。

1. 新生代:Scavenge 算法

新生代(New Space)通常容量为 1-8MB,用于存放新创建的短生命周期对象。Scavenge 算法基于 Cheney 的半空间复制算法,将新生代堆内存等分为两个半空间:From 空间(活动空间)和 To 空间(空闲空间)。

code
Scavenge 回收过程:

  回收前:
  ┌─────────────────────────┐
  │     From 空间(活动)     │     To 空间(空闲)
  │  ┌───┐ ┌───┐ ┌───┐     │     ┌─────────────────┐
  │  │ A │ │ B │ │ C │     │     │                 │
  │  │✓活│ │✗死│ │✓活│     │     │    (空)        │
  │  └───┘ └───┘ └───┘     │     │                 │
  └─────────────────────────┘     └─────────────────┘

  回收后:
  ┌─────────────────────────┐
  │     From 空间(清空)     │     To 空间(活动)
  │  ┌─────────────────┐    │     ┌───┐ ┌───┐
  │  │                 │    │     │ A │ │ C │  ← 存活对象复制过来
  │  │    (空)        │    │     │✓活│ │✓活│    且排列紧凑
  │  │                 │    │     └───┘ └───┘
  └─────────────────────────┘     └─────────────────┘

  最后交换 From/To 角色,为下次回收做准备
javascript
// Scavenge 算法特点:
// 1. 只复制存活对象,死对象直接被遗弃
// 2. 复制后对象排列紧凑,无内存碎片
// 3. 时间复杂度与存活对象数量成正比
// 4. 牺牲一半空间换取效率,适合存活率低的新生代

// 对象晋升条件:
// -经历过一次 Scavenge 回收仍存活 → 晋升到老生代
// -To 空间使用率超过 25% → 直接晋升到老生代

适用场景:新生代对象通常"朝生夕死",存活率极低,Scavenge 只需复制少量存活对象,效率很高。

2. 老生代:Mark-Sweep & Mark-Compact

老生代(Old Space)存放生命周期较长的对象,容量远大于新生代。V8 对老生代采用**标记清除(Mark-Sweep)标记整理(Mark-Compact)**相结合的策略。

code
Mark-Sweep(标记清除):

  标记阶段:从 Root 出发,递归标记所有可达对象
  ┌─────────────────────────────────┐
  │  Root → A → B                   │
  │         ↓                       │
  │         C     D (不可达)         │
  │                                 │
  │  ✓ A, B, C: 已标记(存活)       │
  │  ✗ D: 未标记(死亡)            │
  └─────────────────────────────────┘

  整理阶段:将所有存活对象移动到连续内存,消除碎片
  ┌─────────────────────────────────┐
  │  整理前:                        │
  │  Root → A → B                   │
  │  ┌─────┬─────┬─────┬──────────┐ │
  │  │  A  │  D  │  B  │   空闲    │ │
  │  └─────┴─────┴─────┴──────────┘ │
  │                                 │
  │  整理后:                        │
  │  Root → A → B                   │
  │  ┌───┬───┬───┬───────────────┐  │
  │  │ A │ B │ C │   连续空闲     │  │
  │  └───┴───┴───┴───────────────┘  │
  │  ✓ 无碎片,但移动对象有开销      │
  └─────────────────────────────────┘
javascript
// Mark-Sweep 与 Mark-Compact 的协作策略:
// 1. 优先使用 Mark-Sweep(速度快,不移动对象)
// 2. 当内存碎片过多时,使用 Mark-Compact(整理碎片)
// 3. Mark-Compact 需要移动对象并更新引用,开销较大

// 老生代回收流程:
// 标记 → 清除 → (碎片过多时) 整理

为什么不全用 Mark-Compact? 因为整理需要移动所有存活对象并更新引用指针,开销远大于简单的清除。只有在碎片化严重影响分配时才触发整理。

3. 增量标记(Incremental Marking)

全量标记会导致较长的 GC 停顿(Stop-The-World),影响应用响应性。V8 引入增量标记,将标记任务拆分为多个小步骤,穿插在 JavaScript 执行之间。

code
全量标记(长停顿):
  JS执行 ████████  GC停顿 ████████████  JS执行 ████████
                      ↑ 长时间阻塞

增量标记(短停顿):
  JS ██ 标记 ██ JS ██ 标记 ██ JS ██ 标记 ██ JS ██ 清除 ██ JS ██
         ↑     ↑       ↑     ↑       ↑      短暂停顿
  每次只做一小部分标记工作,避免长时间阻塞主线程
javascript
// 增量标记的三阶段:
// 1. 标记根节点 → 速度快,停顿极短
// 2. 标记引用链 → 拆分为多个小步骤
// 3. 标记完成 → 执行清除

// 写屏障(Write Barrier):
// 增量标记期间,如果 JS 代码修改了已标记对象的引用
// 需要通过写屏障将修改的对象重新加入标记队列
const obj = { ref: oldTarget };
obj.ref = newTarget; // 触发写屏障,确保 newTarget 被正确标记

核心权衡:增量标记用更多的总 GC 时间换取更短的单次停顿,提升用户感知的响应速度。

4. 并发回收(Concurrent GC)

V8 进一步引入并发回收机制,让 GC 的部分工作在辅助线程中执行,主线程继续运行 JavaScript,实现真正的无停顿回收。

code
并发回收示意:

  主线程:    JS执行 ████████████████████████████████████
  辅助线程1:        ████标记████████标记████
  辅助线程2:             ████标记████████清除████
  辅助线程3:                  ████整理████

  主线程几乎不暂停,GC 工作由辅助线程并行完成
javascript
// V8 并发回收策略:

// 1. 并发标记(Concurrent Marking)
//    - 辅助线程执行标记工作,主线程不暂停
//    - 遇到写屏障同步问题,通过原子操作和写屏障协调

// 2. 并发清除(Concurrent Sweeping)
//    - 辅助线程执行清除工作
//    - 主线程可正常分配内存

// 3. 并发整理(Concurrent Compaction)
//    - 辅助线程执行内存整理
//    - 主线程通过页级锁定协调访问

// V8 的完整 GC 策略组合:
// ┌────────────┬──────────┬───────────┬───────────┐
// │            │ 新生代    │ 老生代     │ 全堆       │
// ├────────────┼──────────┼───────────┼───────────┤
// │ 主 GC      │ Scavenge │ Mark-Sweep│ -         │
// │ 增量       │ -        │ Incremental│ -        │
// │ 并发       │ -        │ Concurrent│ Concurrent│
// │ 全堆       │ -        │ -         │ Mark-Compact│
// └────────────┴──────────┴───────────┴───────────┘

并发回收的挑战:主线程和 GC 线程可能同时访问同一块内存,需要通过锁、原子操作和写屏障来保证数据一致性。V8 花费了大量工程努力来降低同步开销。


九、内存优化最佳实践

1. 避免创建不必要的闭包和大对象

javascript
// ❌ 闭包不必要地持有大对象
function setupHandler(data) {
  const hugeData = new Array(1000000).fill('x');
  element.addEventListener('click', () => {
    console.log(hugeData.length); // 闭包持有 hugeData 引用
  });
}

// ✅ 只提取需要的值
function setupHandler(data) {
  const length = data.length;
  element.addEventListener('click', () => {
    console.log(length); // 闭包只持有基本类型值
  });
}

// ❌ 循环中创建大量临时对象
function processItems(items) {
  return items.map(item => ({ value: item, timestamp: Date.now() }));
}

// ✅ 复用对象或使用原始类型
function processItems(items) {
  const now = Date.now();
  return items.map(item => [item, now]); // 数组比对象更轻量
}

2. 及时解除引用

javascript
// ❌ 大对象使用后仍被引用
class DataProcessor {
  process() {
    this.rawData = fetchLargeData(); // 一直挂在 this 上
    const result = analyze(this.rawData);
    return result;
    // this.rawData 永远不会被回收
  }
}

// ✅ 使用后置为 null
class DataProcessor {
  process() {
    let rawData = fetchLargeData();
    const result = analyze(rawData);
    rawData = null; // 允许 GC 回收
    return result;
  }
}

// ✅ 使用块级作用域自动释放
function process() {
  {
    const rawData = fetchLargeData(); // 块级作用域
    const result = analyze(rawData);
    // rawData 在块结束后即可被回收
  }
  // 此处 rawData 已不可达
  return result;
}

3. 谨慎使用全局变量

javascript
// ❌ 全局变量永远不会被回收
window.cachedData = loadLargeDataset();

// ❌ 遗漏声明的变量成为全局变量
function process() {
  result = heavyComputation(); // 隐式全局变量
}

// ✅ 使用模块作用域
const cachedData = loadLargeDataset(); // 模块作用域,可被回收

// ✅ 严格模式防止隐式全局变量
'use strict';
function process() {
  const result = heavyComputation(); // 必须声明
}

// ✅ 需要全局缓存时使用 WeakRef
const globalCache = new Map();
globalCache.set(key, new WeakRef(largeObject));
// largeObject 在没有其他引用时可被回收

4. WeakRef 与 FinalizationRegistry 实战

javascript
// 场景:图片缓存,内存不足时允许 GC 回收
class ImageCache {
  #cache = new Map();
  #registry = new FinalizationRegistry(key => {
    // 对象被回收后清理缓存条目
    this.#cache.delete(key);
  });

  set(key, image) {
    const ref = new WeakRef(image);
    this.#cache.set(key, ref);
    this.#registry.register(image, key);
  }

  get(key) {
    const ref = this.#cache.get(key);
    if (!ref) return undefined;
    return ref.deref(); // 可能返回 undefined(已被 GC)
  }
}

// 使用:缓存大图
const cache = new ImageCache();
const largeImage = loadImage('hero-banner');
cache.set('hero-banner', largeImage);

// 后续获取时,如果内存紧张,largeImage 可能已被 GC 回收
const img = cache.get('hero-banner');
if (!img) {
  // 重新加载
  reloadImage('hero-banner');
}

⚠️ ****WeakRef 和 FinalizationRegistry 不是内存管理的"银弹" - WeakRef.deref() 的结果随时可能变为 undefined,不能依赖其确定性

  • FinalizationRegistry 的回调时机不可控,可能延迟很久才触发
  • 它们适用于可接受数据丢失的缓存场景,不适用于核心业务逻辑

5. 使用 Chrome DevTools Memory 面板排查内存泄漏

code
排查流程:

1. 打开 DevTools → Memory 面板

2. 拍摄堆快照(Heap Snapshot)
   ┌─────────────────────────────────────┐
   │  Snapshot 1  →  操作前基准          │
   │  Snapshot 2  →  执行可疑操作后       │
   │  Snapshot 3  →  再次执行可疑操作后   │
   └─────────────────────────────────────┘

3. 对比快照:选择 Snapshot 2,筛选 "Objects allocated between Snapshot 1 and 2"
   - 关注持续增长的对象类型(如 Detached DOM nodes、闭包等)

4. 查看保留路径(Retainers)
   - 找到阻止 GC 回收的引用链
   - 确认是否需要手动解除引用

5. 使用 Allocation Timeline 记录分配过程
   - 蓝色柱子表示分配但未回收的内存
   - 持续增长的蓝色区域 = 潜在泄漏
javascript
// 常见泄漏模式在 DevTools 中的表现:

// 1. Detached DOM nodes
//    搜索 "Detached",找到脱离 DOM 树但仍被引用的元素
const detached = document.createElement('div');
document.body.appendChild(detached);
document.body.removeChild(detached);
// 如果仍有 JS 变量引用 detached,它不会被回收

// 2. 闭包泄漏
//    在 Retainers 中查看闭包捕获的变量
function createLeak() {
  const leaked = new ArrayBuffer(1024 * 1024);
  return () => leaked; // 闭包持有 leaked 引用
}

// 3. 事件监听器泄漏
//    检查 Event Listeners 面板
//    确认组件销毁时是否移除了所有监听器

十、参考资料


提示:内存泄漏通常不易察觉,需要在开发阶段养成良好的编码习惯。定期使用 DevTools 检查内存使用情况。