写屏障(Write Barrier)是编译器在指针赋值操作中插入的一段额外逻辑用于在并发标记阶段追踪指针变化,确保 GC 不会遗漏任何存活对象。
1. 为什么需要写屏障?
Go 使用的是并发标记(Concurrent Mark)。这意味着在 GC 扫描对象、给对象涂色(三色标记法)的同时,你的业务代码(Goroutines)仍在运行并修改内存中的指针。
如果没有写屏障,可能会发生以下情况(导致对象丢失):
- 场景:GC 已经扫描完了对象 A(标记为黑色),正在扫描其他地方。
- 动作:程序修改了 A 的指针,让 A 指向了一个还未被扫描的对象 B(目前是白色)。
- 动作:程序随后删除了原本指向 B 的所有其他引用。
- 结果:由于 A 已经是黑色的,GC 不会再次扫描 A,因此也就看不到 B。最终 B 会被误当作垃圾清理掉,导致程序崩溃。
写屏障通过监控指针的修改行为,打破了上述“丢失引用”的条件。
2. 写屏障是如何工作的?
当写屏障开启时,每当你执行类似 object.field = ptr 的指针赋值操作时,编译器生成的代码会自动插入一段逻辑。
Go 目前使用的是 混合写屏障(Hybrid Write Barrier)(自 Go 1.8 起),它结合了两种经典的屏障策略:
- 插入写屏障(Dijkstra):当一个对象被引用时,强制把它标记为“灰色”(待扫描)。确保黑色对象不会指向不可达的白色对象。
- 删除写屏障(Yuasa):当一个对象的引用被删除时,如果它之前是白色,就把它标记为“灰色”。确保被删除引用的对象依然能在这个周期内存活。
混合写屏障的逻辑伪代码:
// 当执行:slot = ptr (即将指针 ptr 写入内存槽 slot)
write_barrier(slot, ptr):
shade(*slot) // 将原先引用的旧对象标记为灰色(删除屏障逻辑)
shade(ptr) // 将即将被引用的新对象标记为灰色(插入屏障逻辑)
*slot = ptr // 执行真正的赋值3. 写屏障的生命周期
- 开启:在 标记准备阶段 (Sweep Termination) 的 STW 期间,GC 会通知所有的 P(处理器)开启写屏障。
- 运行:在整个并发标记阶段,写屏障持续生效,虽然会带来微小的性能损耗(额外的指令),但保证了并发安全性。
- 关闭:在 标记终止阶段 (Mark Termination) 的 STW 期间,写屏障被关闭。
4. 写屏障的开销
- CPU 损耗:原本一次简单的内存赋值操作,现在变成了包含逻辑判断和函数调用的操作。
- 栈不使用写屏障:为了极致性能,Go 的写屏障不在栈上触发(只在堆上触发)。这也是为什么在“标记终止阶段”仍需要一个短暂的 STW 来重新扫描栈空间,以确保一致性。
总结
写屏障就像是 GC 期间的“实时监控摄像”。它在不停止程序的情况下,时刻盯着指针的变动,确保即便在程序高速运行、不断修改引用的情况下,GC 也能准确无误地识别出所有存活的对象。