垃圾回收机制
JavaScript 的垃圾回收机制(Garbage Collection,简称 GC)是指自动释放不再使用的内存的过程。 由于 JavaScript 是一种具有自动内存管理(Automatic Memory Management)的语言,开发者不需要手动释放内存, 垃圾回收器会定期查找那些不再被引用的值并释放其占用的内存。
特性 | 内容 |
---|---|
自动内存管理 | JS 引擎自动分配和回收内存 |
判定依据 | 可达性(是否可通过引用链访问) |
主流算法 | 标记-清除、引用计数、分代收集 |
V8 优化方式 | 新生代使用 Scavenge,老生代使用标记-清除 |
开发者注意事项 | 避免内存泄漏、手动解除无用引用 |
JavaScript 内存管理流程
- 内存分配:当创建变量、对象、函数等时,JS 引擎会在内存中分配空间。
- 内存使用:程序运行期间读取和操作这些变量和对象。
- 内存释放:当某些数据不再被引用时,GC 会将其标记为“可回收”,然后释放。
垃圾回收的核心机制
垃圾回收的核心机制是可达性(Reachability)。 垃圾回收器会从根出发,追踪所有能触及的对象,无法触及的对象就被视为“垃圾”。
可达性是指从“根对象”出发,是否能通过引用链找到这个值。
一个值当它可以通过引用链从根(root)访问到,它就是“可达的”,比如:
- 全局变量
- 当前执行上下文中的局部变量
- 闭包中引用的变量
- 被 DOM 或事件监听器持有的引用
常见的垃圾回收算法
标记-清除(Mark-and-Sweep)
这是 JS 中最常用的算法,过程如下:
- 标记阶段:从全局对象出发,递归遍历所有可达对象并做标记。
- 清除阶段:删除未被标记的对象,释放内存。
优点 | 缺点 |
---|---|
实现简单 | 可能产生内存碎片 |
引用计数(Reference Counting)
每个对象维护一个“引用计数”,每被引用一次 +1,解除引用时 -1,计数为 0 时被回收。
问题
不能处理 循环引用
js
const a = {};
const b = {};
a.ref = b;
b.ref = a; // 两个对象都被引用,但其实没用了
1
2
3
4
2
3
4
分代回收(Generational GC)V8 引擎使用
将内存分为不同“代”,对不同生命周期的对象采用不同策略:
- 新生代(Young Generation):存活时间短的小对象,采用 Scavenge 算法(复制 + 标记)
- 老生代(Old Generation):存活时间长的对象,采用 标记-清除 + 压缩算法
V8 引擎的 GC 策略简述(Chrome、Node.js)
V8 的垃圾回收器分为两个区域:
区域 | 特点 | 回收策略 |
---|---|---|
新生代 | 占空间小,回收频率高 | Scavenge(复制回收) |
老生代 | 占空间大,回收频率较低 | Mark-Sweep + Compacting |
Scavenge 过程:
- 内存分为 From 空间和 To 空间
- 活着的对象复制到 To,清空 From,交换角色
晋升机制:
- 如果对象经历多次新生代 GC 仍存活,就被移到老生代
开发者该注意什么?
虽然 GC 是自动的,但不等于不用管内存。应注意:
✅ 减少不必要的引用
js
let obj = { name: 'test' };
// 后续不再用
obj = null; // 手动解除引用,帮助 GC
1
2
3
2
3
✅ 避免全局变量泄露
js
function foo() {
leak = {}; // 未用 var/let/const,leak 成为全局变量!
}
1
2
3
2
3
✅ 注意 DOM 和闭包的引用
js
function bindEvent() {
const el = document.getElementById('btn');
el.onclick = function () {
console.log(el); // 闭包中持有 el 引用
};
}
// 若 el 被移除,但闭包还持有引用,无法 GC
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
📌 闭包中的变量为什么不会被回收?
当我们说「闭包中的变量不会被回收」,更准确地说是:
只要闭包函数还被引用,它所引用的变量就是可达的,因此不会被垃圾回收。
情况 | 变量是否可达 | 会被回收吗 |
---|---|---|
闭包函数仍被引用 | ✅ 是 | ❌ 否 |
闭包函数被释放 | ❌ 否 | ✅ 是 |
✅ 原因分析
- JS 中函数是对象,闭包函数可以被赋值、传递、存储。
- 闭包函数引用了外部变量(其词法环境
[[Environment]]
中存着它们)。 - 只要闭包函数还活着,这些变量就会跟着活着。
✅ 示例说明
js
function outer() {
const secret = 'mySecret';
return function inner() {
console.log(secret); // 引用了 secret
};
}
const fn = outer();
fn(); // 输出 mySecret
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- 这里
secret
是在outer
函数中的局部变量。 inner
函数闭包引用了secret
,并被赋值给变量fn
。- 只要 fn 没有被销毁(如全局变量),secret 就不会被 GC。
✅ 什么情况下会被回收?
当闭包函数本身不再被任何地方引用,闭包中的环境(变量)才会被视为不可达:
js
let fn = outer();
fn = null; // 解除引用,闭包内部的 secret 才有机会被 GC
1
2
2