JavaScript的内存管理是引擎自动完成的,开发者不需要像C++那样手动申请和释放内存,但这并不意味着内存问题不会出现,理解垃圾回收原理能帮助我们规避内存泄漏等问题。

JavaScript内存生命周期
JavaScript的内存管理分为三个阶段:
- 内存分配:当我们声明变量、函数、对象时,引擎会自动为它们分配内存空间
- 内存使用:对分配的内存进行读写操作,比如修改变量值、调用对象方法等
- 内存释放:当分配的内存不再被使用时,垃圾回收器会自动回收这些内存
核心垃圾回收算法
引用计数算法
引用计数是最早的垃圾回收算法,核心思路是跟踪每个值被引用的次数,当引用次数变为0时,就认为该值可以被回收。
具体规则是:当一个变量被声明并赋值时,这个值的引用计数加1;如果这个变量被重新赋值,原来值的引用计数减1;如果一个值的引用计数变为0,就会被垃圾回收器回收。
我们可以用一段伪代码理解这个逻辑:
// 引用计数逻辑示例
let obj1 = { name: 'test' }; // 对象{name:'test'}引用计数为1
let obj2 = obj1; // 引用计数加1,变为2
obj1 = null; // 引用计数减1,变为1
obj2 = null; // 引用计数减1,变为0,对象可被回收
但这种算法有一个致命缺陷:无法处理循环引用的问题。比如两个对象互相引用,即使它们已经不再被其他变量使用,引用计数也不会变为0,导致内存无法被回收。
// 循环引用示例
function createCycle() {
let objA = {};
let objB = {};
objA.ref = objB;
objB.ref = objA;
// 函数执行结束后,objA和objB的引用计数都是1,无法被回收
}
createCycle();
标记清除算法
现代JavaScript引擎普遍采用标记清除算法,解决了循环引用的问题。它的工作流程分为两个阶段:
- 标记阶段:从根对象(比如全局对象window、当前执行上下文的变量对象)出发,遍历所有能访问到的对象,给这些对象标记为可达
- 清除阶段:遍历堆中所有对象,把没有被标记为可达的对象回收,同时清除所有标记,方便下一轮回收
循环引用的对象在标记阶段不会被根对象访问到,因此会被标记为不可达,最终被回收。比如前面的objA和objB,在函数执行结束后,根对象无法访问到它们,所以会被正常回收。
V8引擎的垃圾回收优化
V8引擎作为Chrome和Node.js的JavaScript引擎,对垃圾回收做了很多优化,它把堆内存分为新生代和老生代两个区域:
| 区域 | 存储内容 | 回收算法 |
|---|---|---|
| 新生代 | 存活时间短的对象,比如临时变量、函数内的局部对象 | Scavenge算法,复制存活对象到另一个空间,清空当前空间 |
| 老生代 | 存活时间长的对象,比如全局变量、多次回收后还存活的对象 | 标记清除为主,标记整理为辅,避免内存碎片 |
V8还会采用增量标记、并发回收等方式,把垃圾回收的工作拆分成小块,穿插在JavaScript执行过程中,减少垃圾回收带来的卡顿问题。
规避内存泄漏的建议
即使有自动垃圾回收,我们还是可能因为代码写法不当导致内存泄漏,常见的问题和建议如下:
- 避免意外的全局变量:不使用var在非函数顶层声明变量,或者给未声明的变量赋值,这些都会变成全局变量,直到页面关闭才会被回收
- 及时清除定时器:
setInterval如果没有清除,回调函数和里面的引用对象会一直存活 - 移除事件监听:给DOM元素添加的事件监听,在元素被移除时如果没有移除监听,监听函数和引用的对象也无法被回收
- 避免闭包持有不必要的引用:闭包会保留外部函数的作用域,不要让它持有大量不再需要的对象引用
我们可以通过Chrome DevTools的Memory面板来检测内存泄漏,录制内存快照对比不同时间点的对象变化,找到没有被回收的可疑对象。
JavaScript垃圾回收内存管理引用计数标记清除修改时间:2026-06-29 16:21:34