垃圾回收与内存 系列文章目录《JavaScript 基础与进阶笔记》前期偏基础巩固与常见面试点后续进入闭包、异步、工程化等进阶主题第 01 篇数据类型与类型判断第 02 篇变量声明与作用域第 03 篇闭包与高阶函数第 04 篇函数工厂第 05 篇this 指向与绑定第 06 篇原型与原型链第 07 篇类与继承第 08 篇JS 执行机制与异步队列第 09 篇数组常用方法第 10 篇字符串算法第 11 篇常见手写题合集上第 12 篇常见手写题合集下第 13 篇Promise 与 async/await第 14 篇数据结构基础第 15 篇垃圾回收与内存本文文章目录系列文章目录前言一、核心概念可达性与根1.1 什么是「垃圾」1.2 强引用 vs 弱引用二、经典算法2.1 标记-清除Mark-Sweep2.2 引用计数了解三、V8 分代回收简述四、内存泄漏不是 GC 坏了而是仍被引用4.1 常见场景4.2 闭包与泄漏4.3 Map 缓存 vs WeakMap4.4 SPA 与组件卸载五、WeakMap / WeakSet5.1 WeakMap5.2 WeakSet5.3 与 Map / Set 选型六、WeakRef 与 FinalizationRegistry了解七、Chrome DevTools 内存排查思路7.1 Performance monitor7.2 Heap snapshot堆快照7.3 口述模板7.4 Allocation instrumentation八、实践建议九、速查表十、易混淆点归纳十一、思考与练习总结前言JavaScript不能手动free内存由引擎的垃圾回收GC自动处理。面试与线上排查却常问标记清除怎么理解、V8 为何分代、闭包会不会泄漏、WeakMap 和 Map 差在哪、Chrome 堆快照怎么看。本篇从GC 基本算法 → 分代策略 → 泄漏场景 → 弱引用 API → DevTools 思路串讲并与第 03 篇闭包、第 14 篇 Map/WeakMap 对照。读完后应能解释「什么对象会被回收」以及「如何减少无意常驻内存」。一、核心概念可达性与根1.1 什么是「垃圾」引擎视角无法从根Root通过引用链到达的对象即为可回收垃圾。常见根包括全局对象浏览器window/globalThisNodeglobal当前调用栈上的局部变量与闭包引用的外层变量引擎内部注册表如 DOM 包装对象等实现相关只要对象仍被可达就不会被回收与「代码里是否还有变量名指向它」不完全等同——闭包、全局属性、Map 的强引用都会延长生命周期。1.2 强引用 vs 弱引用类型含义对 GC 的影响强引用普通变量、Map的键只要存在对象不会被回收弱引用WeakMap键、WeakSet值、WeakRef不阻止对象被回收二、经典算法2.1 标记-清除Mark-Sweep现代 JS 引擎的主线思路标记从所有根出发遍历引用图标记所有可达对象。清除堆中未被标记的对象视为垃圾释放内存。可选整理移动存活对象减少碎片Mark-Compact。特点能处理循环引用A 引 B、B 引 A若都不可达根仍会被回收。2.2 引用计数了解为每个对象维护被引用次数降为 0 即回收。缺陷循环引用永远无法降为 0早期 IE 部分 COM 对象问题。JS 主流引擎以标记-清除为主引用计数仅作补充或特定场景。// 循环引用示意现代引擎通常仍能回收若整体不可达functioncreateCycle(){consta{};constb{};a.refb;b.refa;returna;}letxcreateCycle();xnull;// a、b 整体不可达后可被 GC三、V8 分代回收简述V8 将堆分为新生代与老生代按「对象存活时间」采用不同策略降低全堆扫描成本。区域特点常用算法新生代新创建、多数很快死亡Scavenge复制算法From/To 空间互换老生代经历多次 GC 仍存活Mark-Sweep Mark-Compact晋升新生代多次存活的对象移入老生代。面试口述「新对象在新生代用 Scavenge 快速清理活得久的进老生代用标记清除/整理。」不必背 V8 版本细节说清分代动机即可。四、内存泄漏不是 GC 坏了而是仍被引用内存泄漏指程序逻辑上不再需要的对象仍被强引用挂着GC无法回收内存占用持续上升。4.1 常见场景场景原因缓解全局变量挂到window/ 模块单例避免无意全局路由切换清理闭包外层变量被内层函数长期引用用完置null缩小闭包粒度第 03 篇** forgotten 监听器 / 定时器**DOM 移除但回调仍注册removeEventListener、clearIntervalMap 缓存无限增长强引用键值永不删LRU 上限、WeakMap、定期清理Detached DOMJS 仍引用已从文档移除的节点移除时解绑引用控制台引用DevTools 选中对象会阻止回收排查时勿长期$0持有4.2 闭包与泄漏闭包本身不是泄漏若闭包捕获的大对象生命周期应与页面一样长却永久挂在全局/单例上才会出问题。functionleak(){consthugenewArray(1e6).fill(x);returnfunction(){console.log(huge.length);// huge 随闭包常驻};}constholdleak();// hold 在 → huge 在// 不再需要时holdnull;4.3 Map 缓存 vs WeakMap/* ❌ Map 字符串键cache 只增不减 → 泄漏风险 */constcachenewMap();constprocess(data){constkeyJSON.stringify(data);if(!cache.has(key))cache.set(key,expensiveCompute(data));returncache.get(key);};/* ✅ 对象作键且随对象生命周期WeakMap */constweakCachenewWeakMap();constprocessSafe(obj){if(!weakCache.has(obj)){weakCache.set(obj,expensiveCompute(obj));}returnweakCache.get(obj);};WeakMap键必须是对象键为弱引用对象无其他引用时可被 GC对应条目自动消失。不可遍历、无size。4.4 SPA 与组件卸载路由离开、组件unmount时清除setInterval/setTimeoutabort未完成请求AbortControlleroff事件总线 / 自定义订阅释放大数组、图表实例等对ref的引用五、WeakMap / WeakSet5.1 WeakMap键仅对象弱引用键。用途DOM 节点元数据、对象私有附加数据、与对象生命周期绑定的缓存。限制无forEach、keys、size内容可能随时被 GC。constwmnewWeakMap();consteldocument.createElement(div);wm.set(el,{clickCount:0});// el 从 DOM 移除且无 JS 引用后wm 中条目可被回收5.2 WeakSet值仅对象弱引用。用途标记「已访问 / 已处理」对象不阻止对象被回收。constvisitednewWeakSet();functionwalk(node){if(visited.has(node))return;visited.add(node);// ...}5.3 与 Map / Set 选型需求选择任意类型键、要遍历、要 sizeMap / Set键/值为对象生命周期随对象不需枚举WeakMap / WeakSet六、WeakRef 与 FinalizationRegistry了解ES2021 提供更低层的弱引用能力日常业务少用多用于库与监控。letobj{id:1};constrefnewWeakRef(obj);console.log(ref.deref());// { id: 1 } — 对象还在时constregistrynewFinalizationRegistry((id){console.log(对象${id}已被回收);});registry.register(obj,obj.id);objnull;// 某次 GC 后 ref.deref() 可能为 undefinedregistry 回调异步、不保证时机注意deref()可能返回undefined对象已回收。FinalizationRegistry回调不保证立即或一定执行不能当作destructor做关键清理。清理逻辑仍应靠dispose/unmount等显式生命周期。Node.js 调试可用node --expose-gc后global.gc()手动触发仅开发环境。七、Chrome DevTools 内存排查思路7.1 Performance monitor观察JS heap size是否随操作只升不降需结合业务判断是否正常缓存。7.2 Heap snapshot堆快照打开Memory→Heap snapshot。操作前拍Snapshot A重复可疑操作后拍Snapshot B。选Comparison看# Delta增长的对象类型(string)、(closure)、Detached HTMLElement等。在Retainers里向上查谁还在引用该对象。7.3 口述模板「怀疑泄漏 → 复现路径 → 前后两次快照对比 → 找增量最大的构造函数 → 沿 Retainers 找根引用 → 代码里解除监听/清缓存/缩小闭包。」7.4 Allocation instrumentationAllocation sampling适合定位哪段代码在频繁分配与快照互补。八、实践建议大对象用完对不再需要的引用赋null帮助断开链GC 仍异步。慎挂全局globalThis.xxx hugeData会常驻。缓存要有界LRU、TTL、WeakMap键随对象走。闭包只捕获必要变量必要时拆函数减少捕获。弱引用DOM 元数据、对象扩展用WeakMap不要用 Weak 结构做「需要遍历的缓存列表」。九、速查表概念要点垃圾从根不可达Mark-Sweep标记可达 → 清除未标记分代新生代 Scavenge老生代 Mark-Sweep/Compact泄漏逻辑不要了仍被强引用WeakMap/WeakSet弱引用、不枚举、键/值为对象WeakRefderef()可能 undefined排查堆快照对比 Retainers十、易混淆点归纳闭包 ≠ 一定泄漏长期不必要的强引用才是问题。局部变量在函数执行完且无闭包引用时可回收闭包会延长外层词法环境。WeakMap 不能用字符串作键Map 的键是强引用。置null不是立刻释放内存只是断开引用下次 GC才可能回收。FinalizationRegistry不能替代unmount清理。深拷贝 WeakMap第 11 篇与WeakMap 作缓存是不同话题。十一、思考与练习1.为什么标记-清除能处理循环引用而纯引用计数不行解析标记看从根是否可达两个互相引用但不可达根的对象标记阶段都不会被标记可一并清除。引用计数下两者计数互不为 0。2.WeakMap适合存「用户 id 字符串 → 用户信息」吗解析不适合。键必须是对象用户 id 应用Map并配合TTL/LRU限界。3.组件卸载时忘记clearInterval会导致什么解析回调闭包可能持有组件状态 / DOM定时器本身也被引用造成泄漏或僵尸逻辑。4.堆快照里(closure)增多说明什么解析可能有过多闭包仍被引用需沿 Retainers 查是全局、缓存还是未解绑监听。5.ref.deref()返回undefined表示什么解析原对象已被 GC不应再使用该对象需重新创建或走降级逻辑。总结GC以可达性为准主流标记-清除V8分代新生代 Scavenge、老生代 Mark-Sweep/Compact。泄漏对象仍被强引用全局、闭包、监听、无界 Map、Detached DOM。弱引用WeakMap / WeakSet存对象关联数据WeakRef / FinalizationRegistry了解即可。排查ChromeHeap snapshot对比 Retainers追引用链SPA 强调unmount 清理。下一阶段进入DOM 与渲染DOM 树与节点类型、查询 API、attrvsproperty等系列第 16 篇。