Contents

Golang GC

Warning
本文最后更新于 March 31, 2022,文中内容可能已过时,请谨慎使用。

所有的编程语言都有内存的管理办法,这里面分两大类;一类是编程语言提供了手动管理内存的方式,开发者必须自行对内存进行申请和释放,虽然手动管理相对精准,但是编程麻烦且稍有不慎会造成内存泄露和指针乱踩的后果。另一类是由编程语言提供了垃圾收集机制,开发者不需要手动管理内存,这为开发者提供了很大的便利。

但是,垃圾回收的工作机制并不是完美的。

Golang 回收机制

GC 回收的是什么?

在应用程序中会使用到的两种内存,分别是 堆(Heap)和 栈(Stack),GC 负责回收的是 堆内存,而不是回收栈中的内存。

主要原因是栈是一块专用内存,专门为了函数执行而准备的,存储着函数中的局部变量以及调用栈。除此以外,栈中的数据都有一个特点——简单。比如局部变量不能被函数外访问,所以这块内存用完就可以直接释放。正是因为这个特点,栈中的数据可以通过简单的编译器指令自动清理,并不需要通过 GC 来回收。


Go V1.3 之前的标记清除法

标记清除法(mark and sweep) ,低版本的Go 采用的标记清除法很古老。原理也很简单。垃圾回收分为两个阶段:标记阶段和清除阶段

  • 标记阶段:通过根节点(GC Roots)标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。
  • 清除阶段:清除所有未被标记的对象。
1
2
| --------------- Stop The World 暂停范围 -----------------|
|   启动STW   |   Mark标记  |  Sweep 清除   |    停止 STW   |

但是这样的垃圾回收机制造就了Golang被人诟病的两个槽点:速度慢、效率差

STW(STOP THE WORLD)噩梦 ,在标记清除过程中,会进入一个 “Stop the World” 阶段, 当前运行的所有程序将被暂停。会导致性能下降。

另外标记阶段需要扫描整个 heap,复杂且消耗巨大,最后的清除阶段还会造成heap碎片

Go V1.5 三色标记法

三色标记法是逻辑上抽象出的三种状态。

  • 白色:对象未被标记,该对象将会在本次GC中被清理

  • 灰色:对象还在标记队列中等待

  • 黑色:对象已被标记,该对象不会在本次GC中被清理

  1. 初始状态下,所有的对象都是白色。

https://img1.kiosk007.top/static/images/go/gc/gc_3c1.jpg

  1. 从根节点开始遍历所有对象,把遍历到的对象变成灰色对象(备注:这里变成灰色对象的都是根节点的对象)。

https://img1.kiosk007.top/static/images/go/gc/gc_3c2.jpg

  1. 遍历灰色对象,将灰色对象引用的对象也变成灰色对象,然后将遍历过的灰色对象变成黑色对象。

https://img1.kiosk007.top/static/images/go/gc/gc_3c3.jpg

  1. 循环步骤3,直到灰色对象全部变黑色。

  2. 通过写屏障(write-barrier)检测对象有变化,重复以上操作

  3. 清除所有的白色对象。

以上图为例,最终对象6由于没有灰色对象引用,最终白色标记表里的对象6被当做 垃圾清除。

写屏障机制

下面主要解释一下步骤5 ,什么是”写屏障机制(Write Barrier)“。

三色标记法在垃圾回收过程中是没有 STW 的。程序在垃圾回收的同时也在正常运行中,为了防止一个被没有被引用的对象(实际上是本来被某对象引用但是取消了引用),在过程中又被已经标记为黑色对象引用,造成误清除,所以必须有一个保护机制即写屏障。

https://img1.kiosk007.top/static/images/go/gc/gc_3c4.jpg

如上图,对象2本引用对象三,但是GC过程中对象2放弃取消引用对象3,转为由对象4引用。那么对象4不会再被扫描,对象3无法变成灰色。

正是为了解决漏标的问题,需要使用写屏障机制来避免,写屏障一定是在内存进行写操作之前执行的。一般屏障机制需要满足以下两个原理来打破上面的破坏。

  • 强三色不变式: 强制性的不允许黑色对象引用白色对象。

  • 弱三色不变式:黑色对象可以引用白色对象,白色对象存在其他灰色对象对他的引用,或者可达它的链路上游存在灰色对象

https://img1.kiosk007.top/static/images/go/gc/gc_3c5.jpg

满足上述两种不变式才能做到GC过程中的误清除。

Go 语言使用两种写屏障技术,分别是 Dikstra 提出的插入写屏障和 Yuasa 提出的删除写屏障。

1
2
3
          —— 插入屏障  ...  对象被引用时触发
屏障机制  |
          —— 删除屏障  ...  对象被删除时触发

插入写屏障

插入写屏障:(满足强三色不变式)在A对象引用B对象时,B对象被标记为灰色(不存在黑色对白色对象引用的情况,因为白色会强制变成灰色)

插入写屏障的缺点:

虽然插入写屏障能解决问题,但是 golang 针对栈上对象的赋值却没有捕捉(没有生成写屏障),原因自然是性能损耗和实现复杂度的考虑。这就开了一个例外的口子,有一些黑色的栈对象指向了白色的对象,而回收器却无法感知到。

golang 的解决方法是:最后再 STW 重新扫描一把栈。这个自然就会导致整个协程的赋值器卡顿。这大约需要 10 ~ 100ms。

删除写屏障

删除写屏障:(满足弱三色不变式)被删除的对象如果自身为灰色或者白色,那么被标记为灰色。(保护灰色对象到白色对象的路径不会断掉)

他的问题是GC过程中如果出现了删除引用,那么所删除的对象及其子对象会全部保留,就是害怕被已被标记的黑色对象再引用。

删除写屏障的缺点:

回收精度低,一个对象即便被删除了最后一个指向它的指针依旧可以活过这一轮,在下一轮GC中被清除。

Go V1.8 混合写屏障

V1.5 通过三色标记 和满足强弱三色不变式的写屏障机制实现了垃圾回收。

但是从上面的过程可以看出,无论是满足强三色的插入写屏障还是满足弱三式的删除写屏障。都有他们的问题。

插入写屏障还需要在结束时STW扫描栈,删除写屏障的回收精度太低。所以在 GoV1.8 版本之后引入了 三色标记 + 混合写屏障机制

具体操作:

  1. GC 开始则将栈上的对象全部标记为黑色(之后不再进行二次重复扫描,无需STW)
  2. GC期间,任何创建在栈上的新对象,均为黑色。
  3. 被删除对象标记为灰色。
  4. 被添加对象标记为灰色。

混合写屏障机制满足了变形的强弱三色不变式,(结合了插入、删除写屏障两者的优点)

总结

  • Go V1.3 标记清除法:整个过程需要 STW ,效率底下
  • Go V1.5 三色标记法:堆空间启用写屏障,栈空间不启用,全部扫描一遍后,需要重新再扫描一次栈(需要STW),效率普通
  • Go V1.8 三色标记法+混合写屏障机制:栈空间不启用(栈基本都标记为黑色),堆空间启用,整个过程不需要STW,效率高。