https://mp.weixin.qq.com/s/zeYg07SOBAt9IQpnVtkZKg
https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/#%E5%B1%8F%E9%9A%9C%E6%8A%80%E6%9C%AF

总结

三色并发标记是需要STW的,因为不暂停用户程序逻辑改变对象的引用关系,会导致不正确的标记,引发GC回收错误。为了防止错误回收的产生,最简单的方式是STW。但是STW的过程对用户程序有很大的影响,那怎么保证对象不丢失的情况下合理的尽可能的提高GC效率,减少STW时间呢?我们需要达成以下两种三色不变性中的任意一种:

  1. 强三色不变性:黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象。
  2. 弱三色不变性 :黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径。

为了更通俗的理解三色标记,可以将颜色分一个等级关系:黑色>灰色>白色,对象的引用关系要符合颜色等级的顺序,就是黑色不能直接引用白色。黑色可以黑色,黑色可以到灰色。

现在我们再回头看三色不变性是如何破坏黑色直接引用白色对象关系的。

  1. 强三色不变性,黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象,具体做法是遇到黑色指向白色的对象,强行将白色对象改为灰色对象。这种做法看起比较暴力,不过能保证对象不会误清理,虽然可能会导致一个真正被清理的对象标记为灰色,接着会标记为黑色,本次清理不会被清理掉,会将一个本应该本次被清理的对象留到了下一轮GC。
  2. 看弱三色不变性,黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径。虽然有黑色对象执行白色对象,但是这个白色对象迟早会被标记为灰色对象,最后会被标记为黑色对象。

Go垃圾回收中通过“插入屏障”和“删除屏障”的方式,实现了上述三色不变性,防止对象被误回收。

1. 插入屏障

插入屏障拦截将白色指针插入黑色对象的操作,标记其对应对象为灰色,这样就不存在黑色对象引用了白色对象的情况,满足了强三色不变性。插入屏障的伪代码如下

  1. writePointer(slot,ptr):
  2. shade(ptr)
  3. *slot = ptr

slot指当前下游对象,ptr是新下游对象,直接将新下游对象ptr标记为灰色,让将新下游对象ptr赋值给当前下游对象slot。假设当前对象为CA,则 CA.writePointer(nil,B) 表示CA之前没有下游对象,新添加一个下游对象B,并且将B标记为灰色。CA.writePointer(C,B)表示将当前对象CA的下游对象C更换为B,并将B标记为灰色。

程序跑起来,大部分的其实都是操作在栈上,函数参数啊、函数调用导致的压栈出栈、局部变量啊,协程栈,如果对栈上的写做拦截,将会导致流程代码非常复杂,并且性能下降会非常大,得不偿失。所以“插入屏障”机制,在栈空间的对象操作中不使用。 而仅仅使用在堆空间对象的操作中。

前面无需STW并已经标记完所有堆上的灰色对象。

对于栈上的对象,堆标记过程中引用关系可能有变化,需要重新标记,在标记前,需要启动STW对栈上的对象进行重新三色标记。栈上对象重新标记完,停止STW。

2. 删除屏障

删除屏障也是拦截写操作的,但是是通过保护灰色对象到白色对象的路径不会断来实现的,满足了弱三色不变性。
深入理解屏障技术 - 图1
用户程序删除指针p1,触发删除屏障,所以对象2被标记为灰色对象,并将它加入灰色对象集合
深入理解屏障技术 - 图2
GC程序继续进行三色标记,最终对象1、对象2和对象3都被标记为黑色
深入理解屏障技术 - 图3
「对象2和对象3已经是垃圾了」,但是他们被标记为黑色,也就是本轮CG不会回收。所以说删除屏障的回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮 GC 中被清理掉

3. 混合写屏障

Go1.8版本引入了混合写屏障机制,避免了对栈的重新扫描,大大减少了STW的时间。混合写屏障=插入屏障+删除屏障,它是变形的弱三色不变性,结合了两者的优点。

插入写屏障在标记开始时无需STW,可直接开始,并发进行,但结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;
删除写屏障则需要在GC开始时 STW 扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象,但结束时无需STW。

Go中混合写屏障的目标,「所有的屏障加在堆上」,对栈不加任何屏障操作。

插入屏障+删除屏障 结合能够搞定堆上各种对象操作,也就是说混合写屏障能够搞定堆上所有的情况。那栈上怎么办呢?别急,先看下面Go1.8中混合写屏障操作规则:

  1. GC开始将栈上的对象全部扫描并标记为黑色
  2. GC期间任何在栈新创建的对象都标记为黑色
  3. 堆上被删除的对象标记为灰色
  4. 堆上被添加的对象标记为灰色

可以看到,GC开始和GC期间栈上的所有对象都是直接标记为黑色,避免对象被GC回收,同时也到达不用屏障的目标。