12-垃圾回收与内存泄漏
2026/6/24 9:33:50 网站建设 项目流程

垃圾回收与内存泄漏

理解 V8 垃圾回收机制,识别和修复内存泄漏,写出内存友好的 JavaScript 代码


学习目标

读完本文,你将学会:

  • 理解 V8 引擎的垃圾回收算法(分代、标记-清除、标记-整理)
  • 识别常见的内存泄漏场景
  • 使用 Chrome DevTools 分析内存问题
  • 掌握 WeakRef 和 FinalizationRegistry 的使用

一、内存管理基础

1.1 JavaScript 的内存生命周期

分配 → 使用 → 释放 ↑ ↑ 变量声明 垃圾回收器自动处理
// 1. 分配内存letobj={name:'Alice',data:newArray(1000000)};// 2. 使用内存console.log(obj.name);// 3. 释放内存(当 obj 不再可达时)obj=null;// 解除引用,等待 GC 回收

1.2 堆与栈

┌─────────────────┐ 高地址 │ 堆 │ 引用类型(对象、数组、函数) │ (动态分配) │ 手动分配,GC 自动回收 ├─────────────────┤ │ 栈 │ 原始类型 + 执行上下文 │ (自动管理) │ 函数调用帧,后进先出 ├─────────────────┤ │ 代码段 │ 可执行代码 ├─────────────────┤ │ 数据段 │ 全局变量、静态数据 └─────────────────┘ 低地址

二、V8 垃圾回收算法

2.1 分代回收

V8 将堆内存分为两代:

┌─────────────────────────────────┐ │ 新生代 │ 容量小(1-8MB),存活时间短 │ ┌─────────┬─────────┐ │ 使用 Scavenge 算法(复制) │ │ From │ To │ │ │ │ 空间 │ 空间 │ │ │ └─────────┴─────────┘ │ ├─────────────────────────────────┤ │ 老生代 │ 容量大,存活时间长 │ │ 使用 Mark-Sweep + Mark-Compact │ │ └─────────────────────────────────┘

晋升条件:对象在新生代经历一次 GC 仍存活,或 To 空间使用率超过 25%。

2.2 标记-清除(Mark-Sweep)

// 算法原理functionmarkAndSweep(){// 1. 标记阶段:从根对象(全局对象、调用栈)出发,遍历所有可达对象constmarked=newSet();constroots=getRoots();// 全局对象 + 当前执行上下文functionmark(obj){if(marked.has(obj))return;marked.add(obj);for(constrefofgetReferences(obj)){mark(ref);}}roots.forEach(mark);// 2. 清除阶段:回收未被标记的对象for(constobjofallObjects){if(!marked.has(obj)){free(obj);}}}

2.3 标记-整理(Mark-Compact)

标记-清除会产生内存碎片。标记-整理在清除后将存活对象向一端移动:

标记-清除后: 标记-整理后: [存活][空闲][存活][空闲][空闲] → [存活][存活][空闲][空闲][空闲] ↑ 碎片 ↑ 连续空间

2.4 增量标记与并发回收

为避免 GC 造成长时间停顿,V8 采用:

  • 增量标记:将标记过程拆分为小步,穿插在 JavaScript 执行之间
  • 并发标记:在辅助线程上并行标记
  • 并行整理:多线程并行整理内存
传统 GC: JS执行 =======[GC暂停==========]====== JS执行 增量 GC: JS执行 ==[GC=]== JS执行 ==[GC=]== JS执行 并发 GC: JS执行 =============== 主线程 GC标记 ============ 辅助线程

三、常见内存泄漏场景

3.1 意外的全局变量

functionleak(){// 未声明的变量变成全局属性accidentalGlobal='泄漏了';// window.accidentalGlobal}// 严格模式可防止'use strict';functionnoLeak(){accidentalGlobal='报错';// ReferenceError}

3.2 闭包引用

functioncreateLeak(){consthugeData=newArray(1000000).fill('x');return{// 只使用了 id,但 hugeData 仍被闭包引用getId:()=>42};}// 修复:只暴露必要数据functioncreateFixed(){constid=42;return{getId:()=>id};}

3.3 被遗忘的定时器和回调

classDataPoller{constructor(){// 组件销毁时忘记清理this.intervalId=setInterval(()=>{this.fetchData();},1000);}destroy(){// 必须清理!clearInterval(this.intervalId);}}

3.4 DOM 引用泄漏

constelements={button:document.getElementById('btn')};// 即使从 DOM 中移除,JS 引用仍存在elements.button.remove();// button 元素仍在内存中!// 修复functionremoveButton(){elements.button.remove();elements.button=null;// 解除引用}

3.5 事件监听器未移除

classEventEmitter{constructor(){this.listeners=newMap();}on(event,fn){if(!this.listeners.has(event)){this.listeners.set(event,newSet());}this.listeners.get(event).add(fn);}off(event,fn){this.listeners.get(event)?.delete(fn);}emit(event,data){this.listeners.get(event)?.forEach(fn=>fn(data));}}

四、内存泄漏检测

4.1 Chrome DevTools Memory 面板

1. 打开 DevTools → Memory 面板 2. 选择 "Heap snapshot" 3. 点击 "Take snapshot"(操作前) 4. 执行 suspected 泄漏操作 5. 再次点击 "Take snapshot"(操作后) 6. 对比两个快照,查看对象增量

4.2 Performance 面板监控

1. 打开 Performance 面板 2. 勾选 Memory 选项 3. 点击录制,执行操作 4. 查看 JS Heap 曲线是否持续上升

4.3 代码中检测

// 监控内存使用(Node.js)constusage=process.memoryUsage();console.log({rss:`${(usage.rss/1024/1024).toFixed(2)}MB`,// 常驻集大小heapTotal:`${(usage.heapTotal/1024/1024).toFixed(2)}MB`,heapUsed:`${(usage.heapUsed/1024/1024).toFixed(2)}MB`,external:`${(usage.external/1024/1024).toFixed(2)}MB`});

完整内存泄漏演示见CODE-ADVANCED/12-垃圾回收与内存泄漏/memory-leak-demo.html


五、WeakRef 与 FinalizationRegistry

5.1 WeakRef

WeakRef持有对象的弱引用,不会阻止垃圾回收:

letobj={data:'重要数据'};constweakRef=newWeakRef(obj);console.log(weakRef.deref());// { data: '重要数据' }obj=null;// 解除强引用// 稍后 GC 可能回收该对象console.log(weakRef.deref());// undefined(可能)

使用场景:大型缓存,允许内存紧张时释放缓存项。

5.2 FinalizationRegistry

在对象被垃圾回收时执行回调:

constregistry=newFinalizationRegistry((heldValue)=>{console.log(`对象已回收,关联值:${heldValue}`);});letobj={name:'临时对象'};registry.register(obj,'可以清理相关资源了');obj=null;// GC 后输出: 对象已回收,关联值: 可以清理相关资源了

完整代码见CODE-ADVANCED/12-垃圾回收与内存泄漏/weakref-demo.js


二、常见误区与注意点

误区正确做法
手动设为 null 就立即回收只是解除引用,实际回收由 GC 决定
闭包一定导致内存泄漏合理设计的闭包是正常用法
WeakMap 的键可以是任意类型WeakMap 键必须是对象
内存泄漏只影响性能严重泄漏会导致页面崩溃(Out of Memory)
现代浏览器没有内存泄漏SPA 长时间运行更容易积累泄漏

三、动手练习

练习 1:找出泄漏代码

以下代码存在内存泄漏,请找出并修复:

classImageGallery{constructor(){this.images=[];document.addEventListener('scroll',this.onScroll);}addImage(src){this.images.push(newImage(src));}}

练习 2:实现 LRU 缓存

使用Map实现一个带容量限制的 LRU(最近最少使用)缓存。


四、AI 辅助学习

4.1 本节知识点的 AI 提问模板

  • “V8 的新生代和老生代分别使用什么回收算法?”
  • “如何通过 Chrome DevTools 定位内存泄漏?”
  • “WeakRef 和 WeakMap 有什么区别?”

4.2 用 AI 验证你的理解

向 AI 描述一段代码,让它分析是否存在内存泄漏及原因。

4.3 警惕 AI 的常见错误

  • AI 可能错误地说delete obj.property会释放内存(只是删除属性,对象仍在)
  • AI 可能忽略闭包中未使用但仍被引用的变量

五、配套代码

本文示例代码位于:CODE-ADVANCED/12-垃圾回收与内存泄漏/

文件名说明
memory-leak-demo.html内存泄漏场景演示与检测
weakref-demo.jsWeakRef 与 FinalizationRegistry 用法
gc-visualization.html垃圾回收可视化动画

六、本章小结

  • V8 使用分代回收:新生代用 Scavenge(复制),老生代用 Mark-Sweep-Compact
  • 常见泄漏:全局变量、未清理的定时器、DOM 引用、事件监听
  • Chrome DevTools 的 Memory 和 Performance 面板是检测利器
  • WeakRef 和 FinalizationRegistry 用于高级内存管理场景

如果本文对你有帮助,欢迎点赞、收藏、关注专栏。有任何问题可以在评论区交流!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询