前言

go 语言的 GC 代码可以在源码文件 src/runtime/mgc.go 看到,其注释看门见山地概括了 go 的 GC:

The GC runs concurrently with mutator threads, is type accurate (aka precise), allows multiple GC thread to run in parallel. It is a concurrent mark and sweep that uses a write barrier. It is non-generational and non-compacting. Allocation is done using size segregated per P allocation areas to minimize fragmentation while eliminating locks in the common case.

总结为一句话就是:go 的 GC 算法是非分代、非紧缩、写屏障的三色并发标记清理算法。

  • 非分代:go GC 并没有像 Java 一样分新生代和老年代,所以也不存在 minor gc 和 full gc 之分
  • 非紧缩:go GC 之后不会整理内存,清理内存碎片
  • 写屏障:类似于 Java 的 G1(Garbage-First) 垃圾收集器中的并发标记,写屏障保证了 GC 期间用户线程 (mutator) 进行的内存更改能被监视,避免了标记遗漏和错误标记

标记清除(Mark And Sweep)算法

该算法主要有两个步骤:标记和清除。标记阶段对对象进行可达性分析,并进行标记。清除阶段则回收未被标记的对象。

image-20191030164145042

图片所示就是标记清除的全过程,标记清除虽然简单,但是这也带来了很多问题:

  1. STW(stop the world): 标记清除在执行1之前要停止程序,并在4之后恢复运行,执行 GC 期间需要暂停用户线程,造成程序卡顿
  2. 标记对象时需要扫描整个 heap,费时费力
  3. 清除数据后会产生不连续的 heap 碎片

三个问题中,STW 是首要问题,于是 go 使用了三色并发标记法来解决。

三色并发标记法:基本步骤

三色并发标记法为对象定义了三种颜色(状态):

  1. 黑色:对象在这次 GC 中已标记,且这个对象包含的子对象也已标记
  2. 灰色:对象在这次 GC 中已标记, 但这个对象包含的子对象未标记
  3. 白色:对象在这次 GC 中未标记

image-20191030170547670

步骤解析:

  1. 程序创建的对象都标记为白色
  2. gc 开始:扫描所有可到达的对象,标记为灰色
  3. 从灰色对象中找到其引用对象标记为灰色,把灰色对象本身标记为黑色
  4. 监视对象中的内存修改,并持续上一步的操作,直到灰色标记的对象不存在
  5. gc 回收白色对象
  6. 最后,将所有黑色对象变为白色,并重复以上所有过程

三色并发标记法:GC 和用户线程的并行

mark and sweep 的 STW 操作,就是 runtime 把所有的线程全部冻结掉,所有的线程全部冻结意味着用户逻辑是暂停的。这样所有的对象都不会被修改了,这时候去扫描是绝对安全的。

go 如何减短这个过程呢?mark and sweep 包含两部分逻辑:标记和清除。 我们知道三色标记法中最后只剩下的黑白两种对象,黑色对象是程序恢复后接着使用的对象,如果不碰触黑色对象,只清除白色的对象,肯定不会影响程序逻辑。所以清除操作和用户逻辑可以并发。

但是标记操作和用户逻辑也是并发的,用户逻辑会时常生成对象或者改变对象的引用,那么标记和用户逻辑如何并发呢?

并发问题一:process 新生成对象的时候,GC 该如何操作呢?

如下图,process 在标记阶段生成一个新对象,我们可能会这么认为:

image-20191030171908778

但是这样显然是不对的,因为按照三色标记法的步骤,这样新生成的对象 A 最后会被清除掉,这样会影响程序逻辑。Golang 为了解决这个问题,引入了写屏障这个机制。

写屏障:该屏障之前的写操作和之后的写操作相比,先被系统其它组件感知。 通俗的讲:就是在 gc 跑的过程中,可以监控对象的内存修改,并对对象进行重新标记。(实际上也是超短暂的 STW,然后对对象进行标记)

在上述情况中,新生成的对象,一律都标为灰色! 即下图:

image-20191030172200747

并发问题二:灰色或者黑色对象的引用改为白色对象的时候,Golang 是该如何操作?

看如下图,一个黑色对象引用了曾经标记的白色对象。

image-20191030172355846

这时候,写屏障机制被触发,向 GC 发送信号,GC 重新扫描对象并标位灰色。

image-20191030172522998

因此,GC 一旦开始,无论是创建对象还是对象的引用改变,都会先变为灰色。

Go GC 的未来

尽管目前 go GC 相比以往版本已经大有改进,效率也今非昔比,但是仍存在一些痛点,所以 go 团队有意在未来的版本尝试 Java 上比较先进的分代思想。

分代思想主要是能解决当前 go 的 GC 频繁问题,在标记阶段 go 需要一定的 CPU 资源来 Mark Scan 所有对象,导致 GC 的 CPU 消耗比较高。

另外,相对于增加 CPU 消耗(比如写屏障)的方案, Go 团队会更倾向于占用内存多一些方案。因为 Go 团队认为,CPU 的摩尔定律发展已经减缓,18个月翻倍减缓为2年,4年…而内存容量和价格的摩尔定律仍在继续。一个稍微更占用内存的解决方案比更占用 CPU 的解决方案拥有更好的扩展性。