JS垃圾回收按需触发而非定时,核心是标记-清除与分代策略;内存泄漏源于对象被隐性引用链钉在根上,五类泄漏源对应五种“引用钉子”,需用DevTools定位并守好声明、资源、引用三道防线。

直接从“为什么回收不了”切入,比背算法更有效。JS 垃圾回收不是定时擦黑板,而是按需触发的标记-清除过程;内存泄漏的本质,是对象本该不可达,却被意外保留在根可达路径上。学这个,核心不在记名词,而在建立“引用链视角”——你能一眼看出 document.getElementById('x') 后又赋给全局变量,就等于给 DOM 节点续了命。
搞懂 GC 的真实触发逻辑,别被“自动”骗了
现代 V8 引擎不会等内存爆掉才回收。它采用分代策略:新生代(Scavenge)用复制算法快速清理短期对象;老生代(Mark-Sweep-Compact)用标记清除+整理,处理长期存活对象。关键点:
- GC 不是匀速发生的,而是在分配失败、空闲时间或内存压力升高时主动触发
- 主线程执行 JS 时,GC 会暂停脚本(Stop-the-World),一次长停顿可能卡住 100ms+,尤其在渲染密集场景
- 你调用
JSON.stringify(largeObj)或展开大型对象调试,本身就会临时创建大量中间字符串和引用,加剧 GC 压力
五类泄漏源,对应五种“引用钉子”
每种泄漏背后,都有一条隐性引用链把本该死亡的对象钉在根对象上:
-
未声明变量 → 自动挂到
window上,成为永久引用 -
未清除的定时器 →
setInterval回调闭包持有外部作用域变量,只要定时器活着,变量就活 -
未解绑的事件监听器 → 尤其是通过
addEventListener绑在全局节点上的,即使节点已移除,监听器仍持引用 -
闭包过度捕获 → 比如返回一个函数,却把整个大对象、DOM 列表甚至
fetch响应体全闭包进去了 - 缓存未设上限或未清理 → 图片预加载、API 响应缓存、Canvas 纹理等,只增不删,越积越多
用工具定位,而不是靠猜
打开 Chrome DevTools → Memory 面板,三步走:
使用约定式提交(Conventional Commits)生成标准化的 Git 提交信息。适用于以下场景: (1) 暂存更改时需要提交信息; (2) 审查 diff 时希望获得结构化摘要; (3) 编写 PR 描述或变更日志(changelog)条目; (4) 用户明确要求“为本次更改编写提交信息”。 该功能会分析已暂存/未暂存的 diff,并生成按类型(type)划分范围的提交信息。
- 录制堆快照(Heap Snapshot):在疑似泄漏前后各拍一张,用“Comparison”视图筛选“# New”或“# Deleted”项,重点看
Detached DOM tree和增长明显的构造函数(如Array、Object、自定义类) - 录制分配时间线(Allocation Timeline):滚动操作或反复触发某功能,观察内存是否阶梯式上涨且不回落
- 强制触发 GC:点击小垃圾箱图标,看释放量是否合理;若释放极少,说明有强引用链卡住对象
写代码时守住三道防线
预防比修复成本低十倍:
-
声明即约束:所有变量必用
let/const,开启"use strict",杜绝隐式全局 -
资源即契约:凡创建定时器、监听器、WebSocket、Observer,必须配套销毁逻辑(
clearInterval、removeEventListener、unobserve),建议封装成带生命周期的 class -
引用即责任:缓存对象加数量/时效控制;DOM 引用用完及时置
null;大数组/对象不用时主动arr.length = 0或obj = null(仅当确认无其他引用时)
不复杂但容易忽略










