Go 1.8起默认启用的混合写屏障,为解决并发标记中漏标活跃对象问题,通过写指针时同时标记旧值和新值所指对象为灰色来保障三色标记正确性。

Go 1.8 起默认启用的混合写屏障(Hybrid Write Barrier),不是为了“更高级”,而是为了解决并发标记中漏标活跃对象这个致命问题——它用确定性的两步标记,把所有可能被绕过的引用变更都兜住。
为什么三色标记在并发下会漏标对象
三色标记要求始终满足“黑色对象不能指向白色对象”(黑色不变性)。但并发时,Mutator(程序代码)和 GC 标记器同时运行,就可能出现这样的竞态:
- Mutator 把一个灰色对象
A的字段从指向白色对象C改为指向白色对象B - GC 已扫描完
A(把它标黑),但还没来得及扫描B - 结果:
B永远不会被标记,被误判为垃圾回收掉
这就是经典的“漏标”(lost write),混合写屏障正是为此而生——它不依赖 Mutator “别乱动”,而是强制在每次指针写入时做兜底动作。
writePointer 被插入到哪些位置
混合写屏障不是你手动调的函数,而是编译器在生成汇编时,自动在所有可能修改堆上指针的地方插入屏障逻辑。典型场景包括:
立即学习“go语言免费学习笔记(深入)”;
- 结构体字段赋值:
obj.field = &otherObj - 切片元素赋值:
slice[i] = &otherObj - map 赋值(value 是指针类型):
m[key] = &otherObj - 接口赋值(底层是堆对象):
var i interface{} = &otherObj
注意:栈上变量之间的赋值、常量、非指针类型(如 int、string 底层数据)不触发写屏障;只对写入堆对象内部指针字段的行为生效。
混合写屏障实际执行哪两步标记
它的核心逻辑非常直白:每次写指针前,同时标记旧值和新值所指对象为灰色(即加入待扫描队列)。伪代码等价于:
func hybridWriteBarrier(slot *unsafe.Pointer, ptr unsafe.Pointer) {
old := *slot
if gcphase == _GCmark {
if old != nil && heapBitsForObject(old).isPtr() {
shade(old) // 标记旧对象(删除屏障语义)
}
if ptr != nil && heapBitsForObject(ptr).isPtr() {
shade(ptr) // 标记新对象(插入屏障语义)
}
}
*slot = ptr
}
关键点:
- 旧值标记防止它被提前回收(比如
A.field原来指向C,现在改指向B,那C还得活到本轮 GC 结束) - 新值标记确保它不会漏标(
B被及时拉进灰色队列) - 这两步都只在
_GCmark阶段生效,STW 阶段和清扫阶段不触发
开启/关闭混合写屏障会影响什么
你无法在运行时开关它——它是 Go 运行时硬编码的机制。但理解它的影响能帮你避开误区:
- 它让 STW 时间大幅缩短(相比 Go 1.7 的纯插入屏障),因为不需要在开始标记前 STW 扫描整个堆栈
- 它带来轻微的写性能开销:每次指针写入多 2 次条件判断 + 最多 2 次原子操作(
shade实际是 CAS 修改 mark bit) - 它不改变 GC 算法语义:对象仍按三色流转,只是保障了并发下的正确性
- 如果你看到 GC 日志里有大量
assist或mark assist,说明 Mutator 正在帮 GC 补标记——这和写屏障本身无关,而是标记压力大导致的,但混合屏障让这种协助更可控
真正容易被忽略的是:混合写屏障只保护堆内对象的指针字段;它不保护栈上临时变量、不保护 Cgo 分配的内存、也不解决循环引用导致的长期驻留问题——那些得靠代码逻辑和 profile 工具定位。


















