转载:每位 Gopher 都应该了解的 Golang 语言的垃圾回收算法
介绍
关于垃圾回收,比较常见的算法有引用计数、标记清除和分代收集。Golang 语言使用的垃圾回收算法是标记清除。本文主要介绍一下 Golang 语言的垃圾回收算法。
Golang 语言 v1.3 及之前的垃圾回收 - 标记清除
Golang 语言的标记清除垃圾回收算法,为了防止 GC 扫描时内存变化引起的混乱,那么就需要 STW,即 Stop The World,具体在 Golang 语言中是指,在 GC 时,先停止所有 goroutine,再进行垃圾回收,等待垃圾回收结束后再恢复所有被停止的 goroutine。关于 STW 执行流程,可以参考下面这张经典图片。
标记清除:
- 启动 STW,暂停程序的业务逻辑,找出不可达对象和可达对象。
- 将所有可达对象做标记。
- 清除未标记的对象。
- 停止 STW,程序继续执行。
- 循环往复,直到进程程序生命周期结束。
标记清除的缺点:
- STW 需要暂停程序,导致程序卡顿。
- 做标记需要扫描整个 heap(堆)。
- 清除数据会产生 head(堆)碎片。
标记清除的优化:
因为 STW 需要暂停程序,为了减少暂停程序的时间,将清除操作移出 STW 执行周期,但是优化效果不明显,进一步优化请继续阅读下文。
Golang 语言 v1.5 的垃圾回收 - 三色标记
所谓三色标记,实际上只是为了方便叙述而抽象出来的一种说法,三色对应垃圾回收过程中对象的三种状态:
- 白色:对象未被标记,
gcmarkBits
对应位为 0,该对象将会在本次 GC 中被清理。 - 灰色:对象还在标记队列中等待被标记。
- 黑色:对象已被标记,
gcmarkBits
对应位为 0,该对象将会在本次 GC 中被回收。
三色标记:
- 新创建的对象,默认标记为白色。
- 从根节点开始遍历所有白色对象,将遍历到的对象的颜色由白色改为灰色。
- 将灰色对象作为根节点开始遍历所有白色对象,将遍历到的对象的颜色由白色改为灰色,并将作为根节点的灰色对象的颜色由灰色改为黑色。
- 循环往复,直到所有灰色对象的颜色都变为黑色。
- 将剩余的白色对象全部清除。
三色标记的缺点:
一个不被灰色对象可达的白色对象,如果被一个黑色对象引用,将会造成该白色对象丢失的问题。
三色标记的优化:
Golang 官方通过强/弱三色不变性,对三色标记做了优化。强三色不变性,即强制性不允许黑色对象引用白色对象;
弱三色不变性,即黑色对象可以引用白色对象,但是必须满足一个条件,该白色对象必须有灰色对象对它的直接引用,或者是可达链路中包含灰色对象。
具体实现是通过写屏障(Write Barrier),即在 GC 的特定时间开启,开启后指针传递时会把指针标记,被标记的指针在本次 GC 过程中不会被清理,等到下次 GC 时,才会被清理。写屏障的目的就是为了缩短 STW 的时间,让 goroutine 和 GC 同时运行。
Golang 语言中的写屏障分为插入写屏障和删除写屏障。
插入写屏障的含义:
满足强三色不变性,即被引用对象,会被强制标记为灰色。
插入写屏障的缺点:
结束时需要 STW 重新扫描栈,大约需要 10-100ms。
删除写屏障的含义:
满足弱三色不变性,即被删除对象,如果自身为灰色或者白色,会被标记为灰色。
删除写屏障的缺点:
回收精度低,即一个对象即使被删除了,最后一个指向该对象的指针也会等到下一次 GC 回收中才被清理。
Golang 语言 v1.8 的垃圾回收 - 混合写屏障
Golang 语言的团队为了更进一步优化垃圾回收,采用了混合写屏障。
混合写屏障:
- 后续无需 STW,GC 在首次执行时,先将栈上的所有对象都标记为黑色。
- GC 在执行过程中,在栈上新创建的对象,默认被标记为黑色。
- 将被删除的对象标记为灰色。
- 将被添加的对象标记为灰色。
混合写屏障的优点:
混合写屏障,满足弱三色不变性,结合了插入写屏障和删除写屏障的优点。
Golang 语言的 GC 触发方式
- 内存分配阈值,
阈值=上次 GC 内存分配值 * 内存增长率
,其中内存增长率由环境变量GOGC
设定,默认值为 100。每次内存分配时,都会先检查当前内存分配是否已经达到阈值,如果已达到阈值,就会触发 GC,即每当内存分配量将要增长一倍时则触发 GC。 - 定时触发,
src/runtime/proc.go
文件中的forcegcperiod
设定触发 GC 的时间间隔,默认值为 2 分钟。 - 手动触发,通过调用
runtime.GC()
方法,触发 GC。
调式 GC
Golang 语言使用 GODEBUG 调式 GC: GODEBUG=gctrace=1 go run main.go
输出结果:
gc 1 @0.013s 0%: 0.037+0.36+0.004 ms clock, 0.60+0.48/0.81/0.012+0.073 ms cpu, 4->4->0 MB, 5 MB goal, 16 P
gc 2 @0.016s 1%: 0.010+0.24+0.004 ms clock, 0.17+0.29/0.44/0.21+0.064 ms cpu, 4->4->0 MB, 5 MB goal, 16 P
gc 3 @0.019s 1%: 0.069+0.50+0.041 ms clock, 1.1+0.29/0.68/0.13+0.66 ms cpu, 4->4->0 MB, 5 MB goal, 16 P
gc 4 @0.021s 2%: 0.056+0.35+0.041 ms clock, 0.90+0.33/0.67/0.064+0.65 ms cpu, 4->4->0 MB, 5 MB goal, 16 P
gc 5 @0.023s 2%: 0.053+0.27+0.003 ms clock, 0.85+0.42/0.63/0.069+0.057 ms cpu, 4->4->0 MB, 5 MB goal, 16 P
输出结果的含义:
-
gc 1 @0.013s 0%:
表示第 1 次执行 GC,0.013s
表示执行时间。 -
0%
GC 占用进程的进程 CPU 时间的百分比。 -
0.037+0.36+0.004 ms clock
表示 GC 耗时,依次是 STW 清扫的时间,并发标记和扫描的时间,STW 标记的时间,即stop-the-world (STW) sweep termination + concurrent mark and scan + and STW mark termination
。 -
0.60+0.48/0.81/0.012+0.073 ms cpu
GC 占用的 CPU 时间。 -
4->4->0 MB
依次表示堆的大小,GC 后堆的大小,存活堆的大小。 -
5 MB goal
表示整体堆的大小。 -
16 P
表示 CPU 的核心数。 -
GC forced
表示调用runtime.GC()
方法,手动执行 GC。
scvg0: inuse: 6, idle: 12, sys: 18, released: 0, consumed: 18 (MB)
scvg0: inuse: 6, idle: 9, sys: 15, released: 0, consumed: 15 (MB)
GC forced
- inuse:内存使用大小。
- idle:需要清除的空闲内存。
- sys: 系统映射的内存。
- released:释放的系统内存。
- consumed:申请的系统内存。
总结
本文通过 Golang 语言的 v1.3、v1.5 和 v1.8 三个版本的 Golang 语言的算法的演进介绍垃圾回收。实际上几乎每个版本都会涉及垃圾回收的优化,相关代码也越来越复杂。如果读者希望更深入了解垃圾回收相关的内容,建议阅读相关源码。
尽管 Golang 语言可以自动进行垃圾回收,但是 GC 也会消耗资源,尽量还是在编写 Golang 代码的时候减少对象分配的数量,采用对象复用、将小对象组合成大对象或采用精准的数据类型,比如可以使用 int8,绝不使用 int。还可以在编写 Golang 代码的时候,手动触发 GC,将不再使用的内存及时释放。