原文地址
正文
Go 垃圾回收器负责收集不再使用的内存。 实现的算法是并发三色标记和扫描收集器。 在本文中,我们将详细了解标记阶段以及不同颜色的使用。
您可以在 Ken Fox 的“visualizing Garbage Collection Algorithms”中找到有关不同类型垃圾收集器的更多信息。
阅读这篇文章之前请先了解GO内存的管理和分配
标记阶段
此阶段执行内存扫描以了解我们的代码仍在使用哪些内存(block)以及应回收哪些内存(block)。
但是,由于垃圾回收与我们的 Go 程序同时运行,因此它需要一种在扫描时检测内存中潜在变化的方法。 为了解决这个问题,实现了一种写屏障算法,并允许 Go 跟踪任何指针变化。 启用写屏障的唯一条件是短时间停止程序,也称为“Stop the World”:

Go 还在进程开始时为每个处理器启动一个标记工作者,帮助标记内存。然后,一旦roots(内存结构的根节点)被排队进行处理,标记阶段就可以开始遍历和着色内存。
现在让我们举一个简单程序的例子,它可以让我们跟踪标记阶段完成的步骤:
type struct1 struct {
a, b int64
c, d float64
e *struct2
}
type struct2 struct {
f, g int64
h, i float64
}
func main() {
s1 := allocStruct1()
s2 := allocStruct2()
func () {
_ = allocStruct2()
}()
runtime.GC()
fmt.Printf("s1 = %X, s2 = %X\n", &s1, &s2)
}
//go:noinline
func allocStruct1() *struct1 {
return &struct1{
e: allocStruct2(),
}
}
//go:noinline
func allocStruct2() *struct2 {
return &struct2{}
}
由于 struct subStruct 不包含任何指针,因此它存储在专用于对象的span中,不引用其他对象:

这使得垃圾收集器的工作更容易,因为它在标记内存时不必扫描这个span(span区分了需要scan 和 no scan)。
一旦分配完成,我们的程序就会强制垃圾回收器运行一个周期。 这是工作流程:

垃圾回收器从栈开始,并递归地跟随指针遍历内存。 标记为 no scan 的 Span 会停止扫描。 但是,这个过程并不是由同一个 goroutine 完成的; 每个指针都在会加入一个工作池中排队。 然后,后台的标色工作者(前文提到的)将会从这个池子将对象取出,并扫描对象,将对象中指针指向的对象重新放入work pool 中。

标色
现在需要一种方法来跟踪哪些内存已被扫描。 垃圾回收器使用三色算法,其工作方式如下:
- 所有对象一开始都被认为是白色的
- 根对象(堆栈、堆、全局变量)将显示为灰色
完成此主要步骤后,垃圾回收器将:
- 将灰色对象标记为黑色
- 跟随来自该对象的所有指针并将所有引用的对象着色为灰色
然后,它会重复这两个步骤,直到没有更多的对象要着色。 从这一点来看,对象要么是黑色的,要么是白色的。 白色集合代表未被任何其他对象引用并且准备好被回收的对象。
过程如下图:

第一步,所有对象都被认为是白色的。 然后,遍历对象,可到达的对象将变为灰色。 如果对象在标记为无扫描的范围内,则可以将其涂成黑色,因为它不需要扫描:

灰色对象现在排队等待扫描并变为黑色:

对象入队也会发生同样的事情,直到没有更多的对象要处理:

在进程结束时,当白色对象是要收集的对象时,黑色对象是内存中正在使用的对象。 正如我们所见,由于 struct2 的实例是在匿名函数中创建的,并且无法从堆栈中访问,因此它保持白色并且可以清除。
由于每个span中名为 gcmarkBits 的位图属性,颜色在内部实现,该属性通过将相应位设置为 1 来跟踪扫描:

正如我们所见,黑色和灰色的工作方式相同。 该过程的不同之处在于,当黑色对象结束链时,灰色将要扫描的对象排入队列。
垃圾回收进行时停止了世界,将在每个写屏障上所做的更改刷新到工作池并执行剩余的标记。

被折叠的 条评论
为什么被折叠?



