学习目标

理解:

  • JavaScript 为什么需要 GC
  • GC 如何判断对象是否存活
  • GC 如何避免长时间卡顿
  • GC 如何与 JavaScript 一起运行
  • 现代 V8 GC 的整体设计思想

一、为什么需要 GC

JavaScript 没有:

1
2
delete obj;
free(ptr);

对象什么时候释放,由 V8 Garbage Collector 自动决定。

GC 的目标:

回收已经无法访问的对象,释放 Heap 内存。


二、JavaScript 内存模型

Stack(栈)

存放:

  • 局部变量
  • 参数
  • 返回地址
  • 对象引用(Reference)

例如:

1
2
3
function test() {
let a = {};
}

运行时:

1
2
3
4
5
6
Stack

a


Heap Object

Stack 保存的是引用。

不是对象。


Heap(堆)

真正对象存放的位置:

1
2
3
4
5
6
7
{}
[]
Function
Map
Set
Date
DOM Wrapper

GC 回收的是 Heap。


三、GC 如何判断对象是否存活

现代 GC:

不是:

Reference Counting(引用计数)

而是:

Root Reachability(Root 可达性)


Root

GC 从 Root 开始遍历对象图。

主要 Root:

1
2
3
4
5
Global Object

Call Stack

当前执行上下文

例如:

1
let a = {};
1
2
3
4
5
6
7
8
9
Root



a



{}

对象可达。

不会回收。


函数结束:

1
2
3
4
5
function test() {
let a = {};
}

test();
1
2
3
4
5
Root

×

{}

对象不可达。

GC 可回收。


四、循环引用不会导致泄漏

例如:

1
2
3
4
5
let a = {};
let b = {};

a.child = b;
b.parent = a;

对象图:

1
2
3
4
5
6
7
A



B


只要:

1
2
3
4
5
Root

×

A

整个环都会释放。

原因:

现代 GC 使用 Reachability。

不是 Reference Count。


五、Mark-Sweep

GC 分两步:

Mark

从 Root 开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
Root



A



B



C

标记:

1
2
3
4
5
A ✓

B ✓

C ✓

Sweep

所有没有标记:

1
2
3
4
5
D

E

F

全部释放。


六、为什么需要 Stop-The-World(STW)

假设:

GC:

1
扫描 A

同时:

JS:

1
A.child = C;

对象图发生变化:

1
2
3
4
5
6
7
8
9
Root



A



C

GC:

可能已经扫描完 A。

不会再回来。

于是:

C 被漏掉。

因此:

传统 GC:

1
2
3
4
5
6
7
8
9
暂停 JavaScript



扫描对象图



恢复 JavaScript

目的:

保证对象图一致。


七、Incremental Marking(增量标记)

传统:

1
2
3
4
5
Mark



300ms

页面卡顿。

现代:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
JS



Mark 一点



JS



Mark 一点



JS



Mark 一点

一次大暂停。

拆成很多小暂停。


八、Tri-color Marking(三色标记)

对象分三种状态。


White(白)

1
2
3
GC 还没有发现。

当前认为可能是垃圾。

Gray(灰)

1
2
3
4
5
已经发现。

但是:

引用的对象还没有扫描完成。

例如:

1
2
3
4
5
6
7
8
9
Root



A(Gray)



B(White)

Black(黑)

1
2
3
自己扫描完成。

所有引用对象也扫描完成。

例如:

1
2
3
4
5
6
7
8
9
Root



A(Black)



B(Gray)

最终:

1
2
3
4
5
6
7
8
9
Root



A(Black)



B(Black)

九、为什么需要 Write Barrier

问题:

1
2
3
4
5
A(Black)



扫描完成

JS:

1
A.child = C;

变成:

1
2
3
4
5
Black



White

GC:

不会重新扫描 Black。

于是:

C 被漏标。


Write Barrier

执行:

1
A.child = C;

内部概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
修改引用



Write Barrier



通知 GC:

对象图发生变化



GC 加入 WorkList



以后继续扫描

注意:

Write Barrier:

不是负责扫描。

它只是:

通知 GC。


十、GC 调度

GC:

不是:

1
2
3
4
5
Event Loop



GC Task

GC 并不是普通 Task。

而是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
执行 JS



Safe Point



检查是否需要 GC



需要



暂停 JS



GC



恢复 JS

所以:

GC 更像:

Runtime Maintenance

不是:

Business Task


十一、Safe Point(安全点)

Safe Point:

不是:

  • 操作系统提供
  • 浏览器提供
  • Event Loop 提供

而是:

V8 编译器主动插入的检查点。


例如:

源码:

1
2
3
4
5
6
7
8
9
function foo() {

a();

b();

c();

}

概念模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {

a();

CheckSafepoint();

b();

CheckSafepoint();

c();

CheckSafepoint();

}

注意:

这里不是:

1
GC();

而是:

1
CheckSafepoint();

十二、CheckSafepoint

作用:

1
2
3
4
5
6
7
8
9
if (gc_requested) {

PauseJavaScript();

RunGC();

ResumeJavaScript();

}

如果:

1
gc_requested == false

立即返回:

1
继续执行 JS

绝大多数时候:

1
2
3
4
5
CheckSafepoint()



什么都不会发生

十三、为什么需要 Safe Point

GC 必须知道:

1
2
3
4
5
6
7
8
9
Stack



寄存器



局部变量

哪些对象属于 Root。

如果:

CPU:

1
2
3
4
5
对象写了一半

寄存器更新一半

引用关系还没建立

GC:

无法得到一致对象图。

因此:

只能停在:

V8 提前设计好的位置。


十四、GC Scheduler(GC 调度器)

现代 GC:

不是:

1
2
3
每100ms

GC一次

而是:

根据:

1
2
3
4
5
6
7
8
9
10
11
12
13
Heap 使用率



内存压力



增量标记是否完成



后台 GC 状态

决定:

1
gc_requested = true

等待:

1
Safe Point

开始 GC。


十五、GC 线程模型

早期

1
2
3
4
5
6
7
8
9
Main Thread



Mark



Sweep

全部主线程完成。


现代 V8

主线程:

1
2
3
4
5
6
7
8
9
JavaScript

+

Safe Point 检查

+

部分 GC

后台线程:

1
2
3
4
5
Concurrent Mark

Concurrent Sweep

Concurrent Compact

多个线程协同工作。

涉及对象图一致性的阶段:

仍然需要:

短暂 STW。


十六、Write Barrier 与 Safe Point 的区别

Write Barrier

发生:

1
obj.child = other;

作用:

1
2
3
4
5
6
7
8
9
对象图发生变化



通知 GC



避免漏标

解决:

对象图一致性。


Safe Point

发生:

1
执行 JavaScript 过程中

作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
检查:

GC 是否请求暂停?



如果需要



暂停 JS



GC



恢复 JS

解决:

什么时候可以安全暂停 JavaScript。


十七、现代 GC 的完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
           JS



执行 JavaScript



Safe Point



CheckSafepoint()



gc_requested ?

│ │

│否 │是

▼ ▼

继续执行 Pause JS



Incremental GC



Mark / Sweep / Compact



Resume JS



继续执行

十八、当前形成的完整知识体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
JS Object


Heap


Object Graph


Root


Reachability


Mark-Sweep


Stop-The-World


Incremental Marking


Tri-color Marking


Write Barrier


Safe Point


CheckSafepoint


GC Scheduler


Concurrent GC

今天真正掌握的知识

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 是一个持续运行、增量推进、并发协作的运行时系统,而不是一个偶尔执行的大任务。