Skip to content

Latest commit

 

History

History
60 lines (42 loc) · 3.39 KB

File metadata and controls

60 lines (42 loc) · 3.39 KB

垃圾回收器: 内存屏障

[TOC]

Go 的标记清扫分回收器和赋值器两个部分,赋值器在进行标记的过程中,会执行:创建、读取、写入操作。

我们已经知道了 Go 的垃圾回收器是一个三色并发收集器,即每个对象会被标记为白色、灰色或者黑色。在 GC 周期的开始前, 所有对象都是白色,垃圾回收器的目标是将所有可达对象标记为黑。 垃圾回收器通过将 GC 根(主要是栈和全局变量)标记为灰色然后尽力在满足三色不变性(即不存在指向白色对象的黑色对象) 将所有灰色对象标记为黑色。在并发更新指针时候,需要使用屏障技术来控制指针的读写行为,从而确保三色不变性。

Dijkstra 写屏障

Go 1.7 使用了一个简单的 Dijkstra 写屏障,实现如下:

writePointer(slot, ptr):
    shake(ptr)
    *slot = ptr

shade(ptr) 会将尚未编程灰色或黑色的指针 ptr 标记为灰色。通过保守的假设 *slot 可能会变为黑色, 并确保 ptr 不会在将赋值为 *slot 前变为白色,进而确保了强三色不变性。

Dijkstra 屏障与其他类型的屏障相比具有几个优势:

  1. 性能优势:它不需要对指针进行任何处理,因为指针的读操作通常比写操作高出一个或更多数量级。
  2. 前进保障:与 Steele 写屏障不同,对象可从白色到灰色单调转换为黑色,因此总工作量受到堆大小的显示。

当然,Dijkstra 写屏障也有缺点,尤其是它展示了栈指针的权衡: 当栈上的指针进行写操作时,必须插入写屏障。这就使得该操作相当昂贵,或者要求栈必须是恒灰(permagrey)的。

Go 选择了后者,因此在 STW 期间,必须重新对栈进行扫描。垃圾收集器首先在 GC 循环开始时扫描所有栈从而收集根。 但是如果没有栈的写屏障,我们便无法确保堆栈以后不会包含对白色对象的引用,因此扫描栈只有黑色,直到其 goroutine 再次执行, 因此它保守地恢复为灰色。从而在循环结束时,垃圾回收器必须重新扫描灰色堆栈以使其变黑并完成标记任何剩余堆指针。 由于必须保证栈在此期间不会继续更改,因此重新扫描过程在 STW 时发生。

混合写屏障

混合屏障是 Go 1.8 中引入的特性,它结合了 Yuasa 风格的删除写屏障和 Dijkstra 风格的插入写屏障。 其实现如下:

writePointer(slot, ptr):
    shake(*slot)
    if current stack is grey:
        shade(ptr)
    *slot = ptr

换句话说,写屏障屏蔽了其引用的被覆盖的对象,并且,如果尚未扫描当前 goroutine 的堆栈,则屏蔽正在复制的引用。 混合屏障可以消除栈的重扫过程,因为一旦栈被扫描变为黑色,则它会继续保持黑色。因此,它消除了栈重新扫描相关的机制, 包括栈屏障和重扫列表。此外,混合屏障要求将对象分配为黑色(分配白色是一种常见策略,但与此屏障不兼容)。

混合写屏障等同于 IBM 实时 JAVA 实现中使用的 Metronome 中使用的双写屏障。这种情况下,垃圾回收器是增量而非并发的, 但最终必须处理严格限制的世界时间的相同问题。

许可

Go under the hood | CC-BY-NC-ND 4.0 & MIT © changkun