面试官:什么是混合写屏障(Hybrid Write Barrier)机制?
面试回答
“混合写屏障是 Go 1.8 版本引入的垃圾回收(GC)机制,主要是为了解决并发标记阶段可能出现的‘对象丢失’问题,同时避免在标记结束时进行长时间的 STW(Stop The World)来重新扫描栈。
在并发的三色标记法中,如果用户代码同时满足两个条件:第一,一个黑色对象增加了一个指向白色对象的引用;第二,一个灰色对象删除了原来指向该白色对象的引用,那么这个白色对象就会被漏标,最终被错误地当成垃圾回收掉。
为了打破这两个条件,Go 结合了 Dijkstra 的‘插入写屏障’和 Yuasa 的‘删除写屏障’的优点,提出了混合写屏障。它的核心逻辑有四点:
- GC 开始时,将当前栈上的所有对象一次性扫描并标记为黑色。
- GC 期间,任何在栈上新创建的对象,直接标记为黑色。
- 被删除的对象:当堆上的指针被修改时,原本指向的旧对象会被标记为灰色(这是删除写屏障的逻辑,防止对象丢失)。
- 被添加的对象:当堆上的指针被修改时,新指向的对象也会被标记为灰色(这是插入写屏障的逻辑,防止黑色对象直接指向白色对象)。
需要特别注意的是,为了保证函数调用的性能,栈上的内存操作是不开启写屏障的。混合写屏障的精妙之处在于,通过上述四条规则,它既保证了并发 GC 的正确性,又彻底消除了 Go 1.7 之前在标记终止阶段需要 STW 重新扫描栈的痛点,将 STW 的时间大幅缩短到了亚毫秒级别。”
系统讲解
背景:并发标记与对象丢失
在 Go 的追踪式垃圾回收中,并发标记意味着 GC 协程和用户协程(Mutator)是同时运行的。用户协程随时可能修改对象之间的引用关系。
如果并发执行的三色标记法不加任何限制,可能会发生对象丢失(存活对象被错误回收),这必须同时满足两个条件:
- 条件一:一个黑色对象新增了对某个白色对象的引用。
- 条件二:包含该白色对象原有引用的灰色对象,删除了对该白色对象的引用。
为了破坏这两个条件,学术界提出了两种经典的三色不变性(Tri-color Invariant):
- 强三色不变性:强制要求黑色对象绝对不能指向白色对象,只能指向灰色或黑色对象。(破坏条件一)
- 弱三色不变性:允许黑色对象指向白色对象,但前提是必须有其他灰色对象也能通过某种路径访问到这个白色对象。(破坏条件二)
历史方案及其痛点
在混合写屏障出现之前,Go 尝试过其他方案,但都有明显的性能痛点:
1. 插入写屏障 (Dijkstra Write Barrier)
- 原理:满足强三色不变性。当对象 A 引用对象 B 时,将 B 强制标记为灰色。
- 痛点:由于栈的操作非常频繁,为了保证性能,栈上不开启插入写屏障。这导致在并发标记结束后,栈上的黑色对象可能悄悄指向了堆上的白色对象。因此,必须在标记终止阶段进行一次 STW(Stop The World),重新扫描所有的 Goroutine 栈,这通常需要 10~100 毫秒,对延迟敏感的服务是致命的。
2. 删除写屏障 (Yuasa Write Barrier)
- 原理:满足弱三色不变性。当对象 A 删除对对象 B 的引用时,将 B 强制标记为灰色。
- 痛点:回收精度低(被删除的对象即使真的是垃圾,也要等到下一轮 GC 才能回收)。并且在 GC 开始时,必须 STW 扫描整个栈,记录初始快照。
混合写屏障 (Hybrid Write Barrier) 的核心逻辑
Go 1.8 结合了插入写屏障和删除写屏障的优点,提出了混合写屏障。其伪代码逻辑如下:
// 伪代码:当执行 *slot = ptr 时触发
writePointer(slot, ptr):
shade(*slot) // 删除写屏障:将被覆盖的旧对象标记为灰色
if current stack is grey:
shade(ptr) // 插入写屏障:将新指向的对象标记为灰色
*slot = ptr注:Go 1.8 实际实现中为了简化逻辑,无条件地对旧对象和新对象都进行着色(shade)。
具体的四条规则
- GC 开始时,将栈上的对象全部扫描并标记为黑色(之后不再需要重新扫描)。
- GC 期间,任何在栈上创建的新对象,均为黑色。
- 被删除的对象标记为灰色。
- 被添加的对象标记为灰色。
为什么混合写屏障能消除 STW 重新扫描栈?
混合写屏障最核心的贡献就是消除了重新扫描栈的 STW。
因为栈上不开启写屏障,我们最担心的是:栈上的黑色对象指向了堆上的白色对象,而堆上唯一指向该白色对象的灰色引用又被删除了。
但在混合写屏障下:
- 如果堆上的灰色引用被删除,触发规则 3(删除写屏障),该白色对象会被立刻置为灰色,从而得到保护。
- 如果栈上的黑色对象指向了堆上的白色对象,这个白色对象必然是从某个地方获取来的。如果它是从堆上获取的,那么在堆上指针传递的过程中,必定会触发规则 4(插入写屏障)或规则 3(删除写屏障),使得该白色对象变灰。
- 栈上新创建的对象直接是黑色(规则 2),不需要再次扫描。
通过这种机制,混合写屏障完美地保证了并发 GC 的正确性,将 STW 时间压缩到了亚毫秒级(通常 < 0.1ms)。