每位 Gopher 都应该了解的 Golang 语言的垃圾回收算法

转载:每位 Gopher 都应该了解的 Golang 语言的垃圾回收算法

介绍

关于垃圾回收,比较常见的算法有引用计数、标记清除和分代收集。Golang 语言使用的垃圾回收算法是标记清除。本文主要介绍一下 Golang 语言的垃圾回收算法。

Golang 语言 v1.3 及之前的垃圾回收 - 标记清除

Golang 语言的标记清除垃圾回收算法,为了防止 GC 扫描时内存变化引起的混乱,那么就需要 STW,即 Stop The World,具体在 Golang 语言中是指,在 GC 时,先停止所有 goroutine,再进行垃圾回收,等待垃圾回收结束后再恢复所有被停止的 goroutine。关于 STW 执行流程,可以参考下面这张经典图片。

图片来自网络

标记清除:

  1. 启动 STW,暂停程序的业务逻辑,找出不可达对象和可达对象。
  2. 将所有可达对象做标记。
  3. 清除未标记的对象。
  4. 停止 STW,程序继续执行。
  5. 循环往复,直到进程程序生命周期结束。

标记清除的缺点:

  1. STW 需要暂停程序,导致程序卡顿。
  2. 做标记需要扫描整个 heap(堆)。
  3. 清除数据会产生 head(堆)碎片。

标记清除的优化:

因为 STW 需要暂停程序,为了减少暂停程序的时间,将清除操作移出 STW 执行周期,但是优化效果不明显,进一步优化请继续阅读下文。

Golang 语言 v1.5 的垃圾回收 - 三色标记

所谓三色标记,实际上只是为了方便叙述而抽象出来的一种说法,三色对应垃圾回收过程中对象的三种状态:

  • 白色:对象未被标记,gcmarkBits 对应位为 0,该对象将会在本次 GC 中被清理。
  • 灰色:对象还在标记队列中等待被标记。
  • 黑色:对象已被标记,gcmarkBits 对应位为 0,该对象将会在本次 GC 中被回收。

三色标记:

  1. 新创建的对象,默认标记为白色。
  2. 从根节点开始遍历所有白色对象,将遍历到的对象的颜色由白色改为灰色。
  3. 将灰色对象作为根节点开始遍历所有白色对象,将遍历到的对象的颜色由白色改为灰色,并将作为根节点的灰色对象的颜色由灰色改为黑色。
  4. 循环往复,直到所有灰色对象的颜色都变为黑色。
  5. 将剩余的白色对象全部清除。

三色标记的缺点:

一个不被灰色对象可达的白色对象,如果被一个黑色对象引用,将会造成该白色对象丢失的问题。

三色标记的优化:

Golang 官方通过强/弱三色不变性,对三色标记做了优化。强三色不变性,即强制性不允许黑色对象引用白色对象;

弱三色不变性,即黑色对象可以引用白色对象,但是必须满足一个条件,该白色对象必须有灰色对象对它的直接引用,或者是可达链路中包含灰色对象。

具体实现是通过写屏障(Write Barrier),即在 GC 的特定时间开启,开启后指针传递时会把指针标记,被标记的指针在本次 GC 过程中不会被清理,等到下次 GC 时,才会被清理。写屏障的目的就是为了缩短 STW 的时间,让 goroutine 和 GC 同时运行。

Golang 语言中的写屏障分为插入写屏障和删除写屏障。

插入写屏障的含义:

满足强三色不变性,即被引用对象,会被强制标记为灰色。

插入写屏障的缺点:

结束时需要 STW 重新扫描栈,大约需要 10-100ms。

删除写屏障的含义:

满足弱三色不变性,即被删除对象,如果自身为灰色或者白色,会被标记为灰色。

删除写屏障的缺点:

回收精度低,即一个对象即使被删除了,最后一个指向该对象的指针也会等到下一次 GC 回收中才被清理。

Golang 语言 v1.8 的垃圾回收 - 混合写屏障

Golang 语言的团队为了更进一步优化垃圾回收,采用了混合写屏障。

混合写屏障:

  1. 后续无需 STW,GC 在首次执行时,先将栈上的所有对象都标记为黑色。
  2. GC 在执行过程中,在栈上新创建的对象,默认被标记为黑色。
  3. 将被删除的对象标记为灰色。
  4. 将被添加的对象标记为灰色。

混合写屏障的优点:

混合写屏障,满足弱三色不变性,结合了插入写屏障和删除写屏障的优点。

Golang 语言的 GC 触发方式

  1. 内存分配阈值,阈值=上次 GC 内存分配值 * 内存增长率,其中内存增长率由环境变量 GOGC 设定,默认值为 100。每次内存分配时,都会先检查当前内存分配是否已经达到阈值,如果已达到阈值,就会触发 GC,即每当内存分配量将要增长一倍时则触发 GC。
  2. 定时触发,src/runtime/proc.go 文件中的 forcegcperiod 设定触发 GC 的时间间隔,默认值为 2 分钟。
  3. 手动触发,通过调用 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

输出结果的含义:

  1. gc 1 @0.013s 0%: 表示第 1 次执行 GC,0.013s 表示执行时间。
  2. 0% GC 占用进程的进程 CPU 时间的百分比。
  3. 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
  4. 0.60+0.48/0.81/0.012+0.073 ms cpu GC 占用的 CPU 时间。
  5. 4->4->0 MB 依次表示堆的大小,GC 后堆的大小,存活堆的大小。
  6. 5 MB goal 表示整体堆的大小。
  7. 16 P 表示 CPU 的核心数。
  8. 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,将不再使用的内存及时释放。

你可能感兴趣的:(每位 Gopher 都应该了解的 Golang 语言的垃圾回收算法)