2. 写屏障机制
Q: 并发标记清除法的难点是什么?
并发标记清除法的核心难点在于如何保证在用户程序并发修改对象引用时,垃圾回收器仍能正确识别存活对象。
主要难点:
- 对象消失问题:在标记过程中,如果用户程序删除了从黑色对象到白色对象的引用,同时从灰色对象到该白色对象的引用也被删除,这个白色对象就会被错误回收,但它实际上还是可达的
- 新对象处理:标记期间新分配的对象如何着色?如果标记为白色可能被误回收,标记为黑色可能造成浮动垃圾
经典示例:
初始状态:黑色对象 C 指向灰色对象 A,而 A 指向白色对象 B
C.ref3 = C.ref2.ref1 // 赋值器并发地将黑色对象 C 指向了白色对象 B
A.ref1 = nil // 移除灰色对象 A 对白色对象 B 的引用
最终状态:白色对象 B 永远不会被标记为黑色对象(回收器不会重新扫描黑色对象),进而对象 B 被错误地回收Q: Go语言是如何解决并发标记清除时,用户程序并发修改对象引用问题的?
Go通过写屏障技术和三色不变性维护来解决这个并发安全问题。
核心挑战是防止"对象消失"现象:当黑色对象新增对白色对象的引用,同时灰色到白色的引用被删除时,白色对象可能被错误回收。
Go采用混合写屏障策略,在指针赋值时执行额外逻辑:
- 新建引用时将目标对象标为灰色
- 删除引用时将被删对象标为灰色
这样确保关键对象不会丢失在标记过程中。
同时Go维护了弱三色不变性:允许黑色对象指向白色对象,但要保证从白色对象出发存在全灰色路径可达根对象。
栈操作因为频繁且开销敏感,没有采用写屏障,而是做了特殊处理:标记开始和结束时分别扫描栈,中间过程不加写屏障。
这套机制让Go实现了微秒级STW时间,大部分GC工作都与用户程序并发执行,在保证回收正确性的同时将性能影响降到最低。
Q: 什么是写屏障、混合写屏障,如何实现?
写屏障的本质是编译器在指针赋值操作中插入的额外很短的指令,当执行 *slot = ptr 这样的指针赋值时,写屏障会在赋值前后执行特定逻辑来标记相关对象,防止并发标记过程中对象被错误回收。
传统写屏障
Dijkstra插入写屏障:在建立新引用时将目标对象标为灰色,但删除引用时无保护
Yuasa删除写屏障:在删除引用时将原对象标为灰色,但新建引用时无保护
两者各有局限性。
Go的混合写屏障
Go 1.8后采用的混合写屏障,结合两者优点:
// 混合写屏障伪代码
writePointer(slot, ptr) {
shade(*slot) // 删除写屏障:标记旧值
if current stack is grey {
shade(ptr) // 插入写屏障:标记新值
}
*slot = ptr
}关键优化:
- 在堆上建立新引用和删除引用时分别采用插入写屏障和删除写屏障的做法
- 栈优化:任何在GC标记阶段被创建于栈上的新对象,默认都标记为黑色。这样GC就不需要关心栈上的指针指向堆里的哪个白色对象了,因为栈本身就被看作是黑色的,它指向的对象必须是可达的
- 不再需要STW去重扫栈,大大减少了停顿时间
核心思想:通过在关键的指针操作时插入标记逻辑,确保并发环境下不会有存活对象被误回收。