Golang GC
Table of Contents
1. GC 算法
经典的 GC 算法有:引用计数(reference counting),复制算法(copying),标记-清扫(mark and sweep)等等。
Golang 使用的 GC 算法是一种“标记-清扫”算法。
1.1. “标记-清扫”算法
“标记-清扫”算法如同它的名字一样,算法分为“标记”和“清除”两个阶段:先“标记”出所有可以回收的对象,在标记完成后统一“清除”所有被标记的对象。
“标记-清扫”算法有下面缺点:
1、算法在执行的时候,需要暂停整个程序!即 stop the world, STW。
2、清除不用的内存时会产生内存碎片。
其中,STW 是最主要的缺点,Golang 中采用了一些措施来尽量缩短 STW 的时间。
1.2. Golang GC 历史
Golang GC 如图 1 (摘自 Go GC: Latency Problem Solved)所示。
Figure 1: GC Algorithm Phases
Golang GC 历史如表 1 所示(摘自:用 GODEBUG 看 GC)。
版本 | GC 算法 | STW 时间 |
---|---|---|
Go 1.0 | 简单的标记-清扫算法(强依赖 tcmalloc) | 百ms到秒级别 |
Go 1.3 | 标记阶段 STW, 清扫阶段并行执行 | 百ms级别 |
Go 1.5 | 三色标记法, 并发标记清扫 | 10-50ms级别 |
Go 1.6 | 小优化。 | 5ms级别 |
Go 1.7 | 小优化。GC 时由 mark 栈收缩改为并发,tcmalloc 中 span 对象分配机制由 freelist 改为 bitmap 模式 | ms级别 |
Go 1.8 | write barrier 切换到 hybrid write barrier , 消除 re-scanning stack | sub ms |
Go 1.12 | Mark Termination 流程优化 | sub ms, 但几乎减少一半 |
2. 三色标记法
2.1. 朴素方法(两色标记法)
在“标记-清扫”算法中,如何标记可回收对象呢?先介绍一种朴素的方法:初始时,所有对象都是白色,从 GC Root 出发,把 GC Root 可达的所有对象都标记为黑色,标记结束后,还处于白色的对象是可以回收的。
2.2. 增加灰色
三色标记法增加了灰色中间状态。在三色标记法中,对象有三种颜色:
黑色(Black):表示对象是 GC Root 可达的,即使用中的对象,黑色是“已经被扫描的对象”。
灰色(Grey):表示被黑色对象直接引用的对象,但还没对它进行扫描。
白色(White):白色是对象的初始颜色,如果扫描完成后,对象依然还是白色的,说明此对象是垃圾对象。
三色标记法中有约束规则:黑色对象不能直接指向白色对象。 即黑色可以指向灰色,灰色可以指向白色。
2.3. 三色标记法主要流程
2.4. 优点(增量式标记)
三色标记法的标记过程可以增量式(Incremental)地运行:GC 线程在遍历一部分的灰色对象后,应用线程可以接着运行,然后 GC 线程接着遍历其它灰色对象。这样, STW 的时间被拆为了多个小段。
注:三色标记法增量式运行时,应用线程可能会破坏三色标记法的约束规则(黑色对象不能直接指向白色对象),需要“写屏障”来避免这种情况出现。
2.5. 写屏障(Write Barrier)
为什么要有写屏障(Write Barrier)呢?前面提到过,是为了防止标记过程增量进行时,应用线程(goroutine)破坏三色标记法的约束规则(黑色对象不能直接指向白色对象)。
在 Go 1.7 的版本是“Dijkstra 写屏障” ,这个写屏障只监控堆上指针数据的变动,由于成本原因,没有监控栈上指针的变动,由于应用 goroutine 和 GC 的标记 goroutine 都在运行,当栈上的指针指向的对象变更为白色对象时,这个白色对象应当标记为黑色,需要再次扫描全局变量和栈,以免释放这类不该释放的对象。
在 Go 1.8 及以后的版本引入了“混合写屏障”,这个写屏障依然不监控栈上指针的变动,但是它的策略,使得无需再次扫描栈和全局变量,不过依然需要STW然后进行一些检查。
3. 参考
Go垃圾回收 1:历史和原理: https://cloud.tencent.com/developer/article/1526095
Go GC: Latency Problem Solved: https://talks.golang.org/2015/go-gc.pdf
用 GODEBUG 看 GC: https://segmentfault.com/a/1190000020255157