前置概念:两种经典屏障
理解混合写屏障前,需先了解其两个“双亲”:
- 插入写屏障 (Dijkstra):每当建立新引用
A.next = B时,强制将 新对象 B 变灰。它保护的是“新欢”,确保新建立的联系不被漏标。 - 删除写屏障 (Yuasa):每当断开旧引用
A.next = nil时,强制将 旧对象 B 变灰。它保护的是“旧爱”,确保断开的路径仍能被追踪。
混合写屏障:融合与进化
一句话定义
混合写屏障(Hybrid Write Barrier)是 Go 1.8 引入的垃圾回收优化机制。它通过在堆内存操作时同时执行“提拔新引用”与“保护旧引用”的双向策略,来补偿栈空间不开启屏障带来的安全风险。该机制彻底消除了标记结束阶段需要暂停程序来重新扫描栈(Stack Rescan)的过程,将 Stop The World (STW) 时间缩减至亚毫秒级。
屏障机制对比
核心技巧:提拔下游白色对象,而非退回黑色对象。
| 屏障类型 | 触发动作 | 具体做法 | 维护原则 | 评价 |
|---|---|---|---|---|
| 插入 | A.next = B | 新引用 B 标灰 | 强三色不变性 | 简单,但需 STW 重扫栈 |
| 删除 | A.next = nil | 旧引用 B 标灰 | 弱三色不变性 | 精度低(浮动垃圾多) |
| 混合 | 建立/断开 | 新旧引用 均标灰 | 结合两者 | 消除重扫,性能极佳 |
为什么不把黑色退回灰色?退回黑色需重新扫描其所有字段,产生大量重复计算,且可能导致标记阶段因引用频繁修改而无法收敛。
核心规则:性能与安全的极致平衡
设计哲学:牺牲极小内存(浮动垃圾),换取几乎无感的 STW 时长。
| 维度 | 核心逻辑 | 设计意图 |
|---|---|---|
| 性能 | 栈区域全黑:STW 一次性扫描栈指针并标黑 | 消除重扫:栈操作从此免除屏障损耗 |
| 性能 | 栈新对象全黑:GC 期间栈上新对象直接标黑 | 无需回头:确保新对象在本轮绝对安全 |
| 安全 | 被删除引用变灰:堆对象断开引用时,旧引用变灰 | 补偿栈风险:防止对象因路径断开被误杀 |
| 安全 | 新添加引用变灰:堆对象建立新引用时,新引用变灰 | 强化路径:确保新引用关系被追踪 |
浮动垃圾指在本轮 GC 中本该被回收,但因为写屏障的保护(变黑)而被迫存活到下一轮的对象。
为什么需要它?
Go 在“性能-安全”博弈中选择了 “堆上双向保护”对冲“栈上无屏障”:
深度解析:混合屏障的“对冲”逻辑Go 栈上不开启写屏障以保证执行效率,但也带来了“黑栈引白堆”的漏标风险。混合屏障引入删除逻辑作为补偿:
- 只要堆引用断开,就立刻变灰。即使对象被“偷偷”挂载到无屏障的栈上,它也已进入灰色队列。
Q:黑色栈会“凭空”引用无引用的白色堆对象吗?不可能。对象不会瞬移,进入黑栈视野必须经过“拷贝”或“新建”。
- 新建对象:GC 期间栈上新建对象直接标黑,天生安全。
- 已有对象:必须从堆或其他栈拷贝引用。只要拷贝后脱离了原有的堆引用链,删除屏障就会立刻将其变灰“保底”。
知识扩展
- STW 优化:STW 仅存在于开启和关闭屏障的瞬间。
- 流转路径:被提拔对象遵循
白 -> 灰 -> 黑。变黑即获“本轮免死金牌”。 - 触发条件:仅在 堆对象 指针修改时触发,栈上修改不触发。