一、内存管理:JS世界的自动清洁工
想象你正在玩一款开放世界游戏,场景中的NPC会不断生成和消失。如果消失的NPC不及时清理,内存很快会被占满导致游戏崩溃。JavaScript的垃圾回收器(GC)就是游戏世界的自动清洁工,它的任务是:
- 标记不再使用的对象(垃圾)
- 回收这些对象占用的内存
- 整理内存碎片提升利用率
但不同对象的生命周期差异极大,于是V8引擎祭出了杀手锏——分代回收策略。
二、新生代:临时对象的快速通道
1. 适用场景
- 存活时间短的对象(如函数局部变量、循环临时变量)
- 内存区域较小(1-8MB)
2. Scavenge算法:双空间搬运工
内存被均分为From-Space和To-Space:
// 伪代码示意
function scavengeGC() {
// 1. 标记存活对象
let liveObjects = markLiveObjects(fromSpace);
// 2. 复制到To-Space
copyToSpace(liveObjects, toSpace);
// 3. 角色互换
[fromSpace, toSpace] = [toSpace, fromSpace];
}
过程动画:
(想象两间教室,每次打扫时把好课桌搬到对面教室,然后清空原教室)
3. 对象晋升
当满足以下条件时,对象会被移到老生代:
- 经历过一次Scavenge回收仍存活
- To-Space使用超过25%(防止空间不足)
三、老生代:长寿对象的重度监护室
1. 适用场景
- 长期存活对象(全局变量、闭包引用)
- 内存较大(几百MB~几GB)
2. 回收算法组合拳
- 标记-清除(Mark-Sweep):标记无用对象后直接清除
- 标记-整理(Mark-Compact):清除后整理内存碎片
// 标记阶段伪代码
function mark(root) {
let queue = [root];
while (queue.length > 0) {
let obj = queue.pop();
if (!obj.marked) {
obj.marked = true;
queue.push(...obj.children);
}
}
}
四、全停顿(Stop-The-World):性能杀手
问题本质
传统GC需要暂停JS主线程,老生代回收可能导致数百毫秒卡顿:
| JS执行 | GC暂停 | JS执行 | GC暂停 |
|-------|--------|-------|--------|
↑ 用户感觉页面卡死
破局三剑客
-
增量标记(Incremental Marking)
把标记过程拆分成多个小任务,穿插在JS任务之间执行。在 2011 年,V8 对老生代的标记进行了优化,从全停顿标记切换到增量标记,但如果一次GC并未执行完毕,又去进行JS代码任务,对象的引用关系被修改了该如何?故而引出了【三色标记法】。 -
并行回收(Parallel)
开启多个辅助线程并行处理,缩短主线程暂停时间。但问题是还会存在主线程的暂停,在量级较大的老生代可能并不适用,新生代量级较小,适合辅线程的并行处理。 -
惰性清理(Lazy Sweeping)
标记完成后不立即清理,等到内存不足时再清理。即每次增量标记完成,如果当前的内存足以支撑代码的运行,那就没必要立刻清理,先让子弹飞一会儿,允许JS代码继续执行,并且也无需一次性全部清理完那些所有非活动对象内存,可以逐步清理直到清理完毕,再去接着执行增量标记任务
五、三色标记法:增量标记的基石
颜色状态机
| 颜色 | 含义 | 示意图 |
|---|---|---|
| 白 | 未访问(待回收) | ⚪️ |
| 灰 | 已访问但子节点未处理 | 🟡 |
| 黑 | 已访问且子节点处理完成 | ⚫️ |
标记流程
graph TD
Root-->A(灰色)
A-->B(灰色)
A-->C(灰色)
B-->D(白色)
C-->E(白色)
style Root fill:#000000,color:#fff
style A fill:#808080
style B fill:#808080
style C fill:#808080
- 从根对象出发,所有直接引用标记为灰色
- 将灰色对象逐个处理:
- 将其子引用标记为灰色
- 自身标记为黑色
- 重复直到没有灰色对象
六、写屏障(Write Barrier):防止漏标的守护者
问题场景
增量标记期间,若已标记为黑色的父对象新增了对白色子对象的引用:
// 伪代码演示问题
let blackObj = { marked: true }; // 已标记为黑色
let whiteObj = { marked: false };
// 在标记过程中新增引用
blackObj.child = whiteObj; // 此时白色对象会被漏标!
写屏障机制
V8通过劫持对象写操作来维护标记正确性:
// 写屏障伪代码
function writeBarrier(parent, child) {
if (isMarking() && isBlack(parent) && isWhite(child)) {
// 将子对象变为灰色重新处理
markGray(child);
addToMarkQueue(child);
}
// 实际执行引用赋值
parent.child = child;
}
七、惰性清理:化整为零的智慧
执行策略
- 标记阶段完成后,内存中已明确黑白对象分布
- 不立即清理所有白色对象,而是:
- 优先分配新对象到空闲区域
- 按需分块清理白色对象区域
优势对比
| 策略 | 单次耗时 | 内存利用率 |
|---|---|---|
| 全量清理 | 高 | 立即提升 |
| 惰性清理 | 低 | 逐步提升 |
八、实战:避免内存泄漏的四大法则
- 警惕隐蔽的全局变量
function leak() {
leakedVar = '这是一个全局变量!'; // 未使用let/const
}
- 及时清理DOM引用
const cache = document.getElementById('oldElement');
document.body.removeChild(oldElement);
// 仍持有cache引用,DOM节点无法被回收!
- 销毁定时器与事件监听
const timer = setInterval(() => {}, 1000);
// 忘记clearInterval(timer)会导致回调函数持续持有闭包
- 谨慎使用闭包
function createClosure() {
const hugeData = new Array(1000000).fill('*');
return () => console.log('闭包持有hugeData!');
}
// 闭包虽好,但注意释放闭包(下一篇教你如何正确使用闭包)
九、调试工具推荐
- Chrome DevTools
- Memory面板:拍摄堆快照分析内存分配
- Performance面板:追踪GC事件与性能指标
十、总结与思考
核心知识点回顾:
- 分代回收:新生代用Scavenge快速清理,老生代用标记组合拳
- 增量标记+写屏障+惰性清理:三位一体解决全停顿问题
- 三色标记法:通过颜色状态实现高效增量处理
十一、知识点梳理Xmind
参考资料:
- V8官方文档 - Memory Management
- 《深入理解JavaScript特性》
- MDN Web Docs - Memory Management
作者:jacklzhang
发布时间:205-02-24
版权声明:转载请注明出处,禁止商用