V8 Garbage Collection
学习目标
理解:
- JavaScript 为什么需要 GC
- GC 如何判断对象是否存活
- GC 如何避免长时间卡顿
- GC 如何与 JavaScript 一起运行
- 现代 V8 GC 的整体设计思想
一、为什么需要 GC
JavaScript 没有:
1 | delete obj; |
对象什么时候释放,由 V8 Garbage Collector 自动决定。
GC 的目标:
回收已经无法访问的对象,释放 Heap 内存。
二、JavaScript 内存模型
Stack(栈)
存放:
- 局部变量
- 参数
- 返回地址
- 对象引用(Reference)
例如:
1 | function test() { |
运行时:
1 | Stack |
Stack 保存的是引用。
不是对象。
Heap(堆)
真正对象存放的位置:
1 | {} |
GC 回收的是 Heap。
三、GC 如何判断对象是否存活
现代 GC:
不是:
Reference Counting(引用计数)
而是:
Root Reachability(Root 可达性)
Root
GC 从 Root 开始遍历对象图。
主要 Root:
1 | Global Object |
例如:
1 | let a = {}; |
1 | Root |
对象可达。
不会回收。
函数结束:
1 | function test() { |
1 | Root |
对象不可达。
GC 可回收。
四、循环引用不会导致泄漏
例如:
1 | let a = {}; |
对象图:
1 | A |
只要:
1 | Root |
整个环都会释放。
原因:
现代 GC 使用 Reachability。
不是 Reference Count。
五、Mark-Sweep
GC 分两步:
Mark
从 Root 开始:
1 | Root |
标记:
1 | A ✓ |
Sweep
所有没有标记:
1 | D |
全部释放。
六、为什么需要 Stop-The-World(STW)
假设:
GC:
1 | 扫描 A |
同时:
JS:
1 | A.child = C; |
对象图发生变化:
1 | Root |
GC:
可能已经扫描完 A。
不会再回来。
于是:
C 被漏掉。
因此:
传统 GC:
1 | 暂停 JavaScript |
目的:
保证对象图一致。
七、Incremental Marking(增量标记)
传统:
1 | Mark |
页面卡顿。
现代:
1 | JS |
一次大暂停。
拆成很多小暂停。
八、Tri-color Marking(三色标记)
对象分三种状态。
White(白)
1 | GC 还没有发现。 |
Gray(灰)
1 | 已经发现。 |
例如:
1 | Root |
Black(黑)
1 | 自己扫描完成。 |
例如:
1 | Root |
最终:
1 | Root |
九、为什么需要 Write Barrier
问题:
1 | A(Black) |
JS:
1 | A.child = C; |
变成:
1 | Black |
GC:
不会重新扫描 Black。
于是:
C 被漏标。
Write Barrier
执行:
1 | A.child = C; |
内部概念:
1 | 修改引用 |
注意:
Write Barrier:
不是负责扫描。
它只是:
通知 GC。
十、GC 调度
GC:
不是:
1 | Event Loop |
GC 并不是普通 Task。
而是:
1 | 执行 JS |
所以:
GC 更像:
Runtime Maintenance
不是:
Business Task
十一、Safe Point(安全点)
Safe Point:
不是:
- 操作系统提供
- 浏览器提供
- Event Loop 提供
而是:
V8 编译器主动插入的检查点。
例如:
源码:
1 | function foo() { |
概念模型:
1 | function foo() { |
注意:
这里不是:
1 | GC(); |
而是:
1 | CheckSafepoint(); |
十二、CheckSafepoint
作用:
1 | if (gc_requested) { |
如果:
1 | gc_requested == false |
立即返回:
1 | 继续执行 JS |
绝大多数时候:
1 | CheckSafepoint() |
十三、为什么需要 Safe Point
GC 必须知道:
1 | Stack |
哪些对象属于 Root。
如果:
CPU:
1 | 对象写了一半 |
GC:
无法得到一致对象图。
因此:
只能停在:
V8 提前设计好的位置。
十四、GC Scheduler(GC 调度器)
现代 GC:
不是:
1 | 每100ms |
而是:
根据:
1 | Heap 使用率 |
决定:
1 | gc_requested = true |
等待:
1 | Safe Point |
开始 GC。
十五、GC 线程模型
早期
1 | Main Thread |
全部主线程完成。
现代 V8
主线程:
1 | JavaScript |
后台线程:
1 | Concurrent Mark |
多个线程协同工作。
涉及对象图一致性的阶段:
仍然需要:
短暂 STW。
十六、Write Barrier 与 Safe Point 的区别
Write Barrier
发生:
1 | obj.child = other; |
作用:
1 | 对象图发生变化 |
解决:
对象图一致性。
Safe Point
发生:
1 | 执行 JavaScript 过程中 |
作用:
1 | 检查: |
解决:
什么时候可以安全暂停 JavaScript。
十七、现代 GC 的完整流程
1 | JS |
十八、当前形成的完整知识体系
1 | JS Object |
今天真正掌握的知识
GC 基础
- JavaScript 为什么需要 GC
- Stack 与 Heap 的区别
- Root 是什么
- Reachability 如何判断对象存活
- 为什么循环引用不会泄漏
GC 算法
- Mark-Sweep
- Stop-The-World
- Incremental Marking
- Tri-color Marking
GC 与 JavaScript 协作
- Write Barrier
- Safe Point
- CheckSafepoint
- GC Scheduler
- 主线程 GC
- 后台并发 GC
总结
①
GC 判断对象是否存活,不看引用计数,而看 Root Reachability(可达性)。
②
Write Barrier 不负责回收对象,它负责通知 GC:”对象图发生变化了。”
③
Safe Point 不执行 GC,它只是检查:”现在是否可以安全暂停并运行 GC?”
④
现代 V8 的 GC 是一个持续运行、增量推进、并发协作的运行时系统,而不是一个偶尔执行的大任务。

