作为 JavaScript 开发者,我们拥有一门 C、C++ 等编译型语言开发者很难拥有的“特权”:几乎不用手动管理内存。
日常开发中,我们可以随意创建变量、对象、函数,无需关心内存分配、释放的底层细节,这些繁琐的工作都由 JavaScript 引擎自动完成。而支撑这一切的核心机制,就是 GC(Garbage Collection,垃圾回收)。
很多开发者只会被动享受 GC 带来的便利,却不了解其底层逻辑。但真正读懂垃圾回收机制,不仅能夯实前端基础,更能帮你写出高性能代码、精准排查内存泄漏、解决隐形性能卡顿问题。
今天我们就从零拆解 JavaScript 垃圾回收,吃透原理、认清陷阱、掌握最优开发实践。
一、什么是垃圾回收(GC)
简单来说,垃圾回收是 JavaScript 引擎的自动内存管理机制,核心作用是回收程序中不再被使用、无价值的对象所占用的堆内存。
我们每创建一个变量、对象、数组,引擎都会自动为其分配内存。随着代码执行,大量数据会失效、废弃,如果没有垃圾回收机制,无用内存会持续堆积,最终引发内存泄漏、页面卡顿、应用闪退、崩溃等问题。
不同于需要开发者手动申请、释放内存的 C/C++,JavaScript 的 GC 是全自动运行的。主流 JS 引擎均内置该机制:
Chrome / Node.js:V8 引擎
Firefox:SpiderMonkey 引擎
Safari:JavaScriptCore 引擎
开发者无法手动控制 GC 执行时机,只能通过规范代码,配合引擎更高效地完成内存回收。
二、GC 核心原则:可达性(Reachability)
JavaScript 判断一个对象是否是“垃圾”,唯一标准就是可达性。
从引擎预设的根对象出发,能够通过引用链直接或间接访问到的对象,都是可达存活对象,不会被回收;反之,无法被访问到的对象,即为不可达垃圾对象,会被 GC 清理。
常见的内存根对象(Roots)
全局对象:浏览器端 window、Node.js 端 global
当前调用栈:正在执行函数的局部变量、函数参数
活跃闭包:闭包作用域中捕获的外部变量
简单总结:只要有一条链路能从根节点找到该对象,它就会常驻内存;彻底断链,就会被回收。
三、现代 JS 核心回收算法:标记-清除(Mark-and-Sweep)
现代所有主流 JavaScript 引擎,均以 标记-清除算法 作为核心垃圾回收方案,它完美解决了传统算法的致命缺陷,支持处理复杂的循环引用场景。
整个回收过程分为两个核心阶段,周期性自动执行:
1. 标记阶段(Mark Phase)
引擎从所有根对象开始遍历,顺着对象的引用链逐层向下搜索,将所有可达、正在使用的对象统一标记为“存活状态”。
可以通俗理解:引擎从起点出发,遍历所有能找到的对象,全部做上“有效、保留”的标记。
2. 清除阶段(Sweep Phase)
标记完成后,引擎扫描整个堆内存,所有未被标记的对象,都会被判定为垃圾,其占用的内存会被统一释放、回收,供后续新对象使用。
核心优势
完美支持循环引用回收。例如对象 A 引用对象 B、对象 B 也引用对象 A,若二者都无法从根节点访问,引擎会精准判定为垃圾,统一回收,不会造成内存滞留。
四、传统算法缺陷:引用计数(Reference Counting)
在标记-清除算法普及前,早期 JS 引擎使用的是引用计数算法,逻辑简单,但存在致命短板,现已被主流引擎淘汰。
工作原理
为每个对象维护一个引用计数器,记录当前被引用的次数。每当新增一次引用,计数器+1;引用解除,计数器-1。当计数器归 0 时,立即回收该对象内存。
致命缺陷
无法处理循环引用。A、B 互相引用,即使二者已经脱离主流程、无法被根节点访问,引用计数永远无法归 0,会永久占用内存,造成顽固内存泄漏。
这也是现代 JS 引擎全面舍弃引用计数、改用标记-清除算法的核心原因。
五、V8 引擎极致优化:分代回收(Generational Collection)
单纯的标记-清除算法需要遍历全部堆内存,全量扫描耗时较长。V8 引擎基于 弱分代假说 做了极致优化:
绝大多数对象生命周期极短,创建后很快就会失效
长期存活的对象,大概率会持续存活
基于该特性,V8 将堆内存划分为两个区域,差异化回收,大幅提升性能:
1. 新生代(Young Generation)
空间小、回收频率高、速度快。所有新创建的对象,默认分配在新生代。大部分短期对象会在这里快速被回收。
2. 老生代(Old Generation)
在新生代中多次存活、长期不失效的对象,会被晋升到老生代。老生代内存空间大、回收频率低,但单次回收更彻底,耗时更长。
这套分代回收机制,避免了引擎频繁全量扫描堆内存,极大降低了 GC 带来的性能损耗,是 V8 高性能的核心关键。
六、前端常见的 5 大内存泄漏场景
GC 不是万能的!不合理的代码写法,会导致无用对象持续被引用、无法被回收,造成内存泄漏。以下是项目中最高频的泄漏场景:
1. 意外创建的全局变量
未使用 let / const / var 声明的变量,会自动挂载到全局对象上,全局对象生命周期与应用一致,永远不会被 GC 回收。
function createLeakyVariable() {
// 未声明,自动变为全局变量
leakyVar = "I'm accidentally global!";
}
createLeakyVariable();2. 未清理的定时器 & 回调
setTimeout、setInterval、DOM 事件监听的回调,会持续持有外部对象引用,不手动清空,对应对象永远无法回收。
let bigData = { /* 超大体积对象 */ };
const button = document.getElementById('myButton');
// 事件监听持续引用 bigData
button.addEventListener('click', function handler() {
console.log(bigData);
});3. 脱离 DOM 的残留引用
手动删除、移除 DOM 节点后,若 JS 代码中仍保留着该 DOM 的引用,节点内存无法被释放,造成内存滞留。
4. 闭包滥用导致变量常驻内存
闭包会捕获外层作用域变量,若闭包长期存活,被捕获的变量会一直常驻内存,无法被 GC 回收,无意间造成内存浪费。
5. 无淘汰机制的全局缓存
将大量数据挂载到全局对象、自定义缓存中,没有过期、淘汰、清空机制,内存会持续累加,最终导致应用内存溢出。
七、JS 内存优化最佳实践(适配 GC 机制)
无需手动管理内存,但可以通过规范代码,让 GC 更高效地工作,从根源避免内存泄漏:
1. 杜绝隐式全局变量
所有变量统一使用 let / const 声明,严格避免变量逃逸到全局作用域。
2. 及时清理副作用
组件销毁、页面卸载时,手动清除定时器(clearTimeout / clearInterval)、移除 DOM 事件监听,切断无效引用。
3. 主动置空长生命周期引用
对于大型对象、长生命周期数据,使用完毕后手动赋值为null,主动切断引用链,触发 GC 回收。
4. 优先使用 WeakMap / WeakSet
二者存储的是弱引用,不会阻止对象被 GC 回收,是存储元数据、缓存临时数据的最优方案。
let user = { name: "Alice" };
const userMetadata = new WeakMap();
// 存储用户元数据
userMetadata.set(user, { lastLogin: new Date() });
// 置空引用,对象可被正常回收
user = null;5. 借助工具精准排查问题
利用 Chrome DevTools 的 Memory、Performance 面板,查看内存波动、生成堆快照,精准定位内存泄漏、内存溢出问题。
八、总结
垃圾回收是 JavaScript 语言易用性的核心基石,解放了开发者的手动内存管理工作。
核心逻辑可以总结为三点:
GC 判断垃圾的核心是可达性,断链即回收;
现代引擎核心算法是标记-清除 + 分代回收,兼顾准确率与性能;
GC 不代表可以随意写代码,规范引用、及时清理、善用弱引用,才能写出高性能、无泄漏的稳健代码。
JavaScript 垃圾回收:原理、机制与内存优化
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。




评论交流
欢迎留下你的想法