
垃圾回收与内存泄漏理解 V8 垃圾回收机制识别和修复内存泄漏写出内存友好的 JavaScript 代码学习目标读完本文你将学会理解 V8 引擎的垃圾回收算法分代、标记-清除、标记-整理识别常见的内存泄漏场景使用 Chrome DevTools 分析内存问题掌握 WeakRef 和 FinalizationRegistry 的使用一、内存管理基础1.1 JavaScript 的内存生命周期分配 → 使用 → 释放 ↑ ↑ 变量声明 垃圾回收器自动处理// 1. 分配内存letobj{name:Alice,data:newArray(1000000)};// 2. 使用内存console.log(obj.name);// 3. 释放内存当 obj 不再可达时objnull;// 解除引用等待 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. 标记阶段从根对象全局对象、调用栈出发遍历所有可达对象constmarkednewSet();constrootsgetRoots();// 全局对象 当前执行上下文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(){consthugeDatanewArray(1000000).fill(x);return{// 只使用了 id但 hugeData 仍被闭包引用getId:()42};}// 修复只暴露必要数据functioncreateFixed(){constid42;return{getId:()id};}3.3 被遗忘的定时器和回调classDataPoller{constructor(){// 组件销毁时忘记清理this.intervalIdsetInterval((){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.buttonnull;// 解除引用}3.5 事件监听器未移除classEventEmitter{constructor(){this.listenersnewMap();}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(fnfn(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.jsconstusageprocess.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 与 FinalizationRegistry5.1 WeakRefWeakRef持有对象的弱引用不会阻止垃圾回收letobj{data:重要数据};constweakRefnewWeakRef(obj);console.log(weakRef.deref());// { data: 重要数据 }objnull;// 解除强引用// 稍后 GC 可能回收该对象console.log(weakRef.deref());// undefined可能使用场景大型缓存允许内存紧张时释放缓存项。5.2 FinalizationRegistry在对象被垃圾回收时执行回调constregistrynewFinalizationRegistry((heldValue){console.log(对象已回收关联值:${heldValue});});letobj{name:临时对象};registry.register(obj,可以清理相关资源了);objnull;// 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 用于高级内存管理场景如果本文对你有帮助欢迎点赞、收藏、关注专栏。有任何问题可以在评论区交流