本文基于Go 1.13
Go的垃圾回收器负责将那些不会再使用的被占用的内存进行回收。实现的算法是并发的三色标记法以及扫描收集器。我们会看一下标记阶段的细节以及不同颜色的使用。
你可以在这篇文章中阅读到不同类型的垃圾回收机制。
标记阶段
这个阶段主要是扫描内存来确认哪一些内存块是仍然被使用,在哪一些内存块是可以被回收的。
然而,由于垃圾回收跟我们的Go程序是并发运行的,所以需要有个方法在扫描进行的同时监测内存的变化。为了解决这个问题,这里会用到写屏障算法并允许Go去跟踪任何一个指针的变化。实现写屏障唯一途径是将程序暂时停止一小段时间,我们称为“全世界静止” (Stop the World)。
在程序运行的开始阶段,每一个processor都有一个负责标记内存的worker。
然后,一旦根节点被入队等待执行,标记阶段就会开始对内存进行遍历和着色。
下面让我们看个小的例子,这个程序允许我们能够遵循标记阶段所完成的步骤
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{}
}
复制代码
由于结构体subStruct
内部不包含任何指针,所以会储存在一个没有指向另一个对象的内存块中:
这会让垃圾清理器更加容易因为当他进行内存扫描的时候不需要去扫描这些内存块。
一旦分配完成,我们的程序会强制让垃圾回收运行一个周期,下面是工作流:
垃圾回收器从栈开始,会追随指针去递归遍历内存。那些被标记为no scan
的内存块会让扫描停止继续扫描。然而,这个过程不是在一个goroutine中完成的。每个指针会在一个垃圾回收器工作池中入队,被goroutine锁消耗出队,出队后找到新的指向再将其重新在垃圾回收器工作池中入队,直至遇到no scan
为止。
着色
worker现在需要有一个途径去跟踪哪些内存已经被扫描过而哪些还没有被扫描、垃圾回收器使用三色标记法如下:
- 最开始阶段所有对象标记成白色
- 根对象(堆、栈、全局变量)会被标记成灰色
这两步都完成以后,垃圾回收器会:
- 拿一个灰色的对象,标记成黑色
- 跟踪这个对象的指针并将其所指向的所有对象都标记成灰色
然后,重复这两个步骤直到没有可以被着色的对象存在为止。从这个角度出发,对象要么是黑色,要么是白色。白色对象代表并没有任何被其他对象的引用,即可以被清除。
这里有个上面步骤的展示
一开始所有对象都是白色,然后从根节点开始递归,所有沿途对象标记成灰色。如果一个对象被标记成no scan
,那可以将它涂成黑色,因为他不需要被继续往后扫描:
现在灰色对象可以入队等待扫描并且转成黑色:
对象以同样的处理方法入队直到没有任何对象需要被处理:
在处理最后一个对象时,黑色的对象就是那个正在使用的内存,而白色的对象就是可以被回收的内存。如我们所见,由于struct2
的实例是在一个匿名函数中创建的,并且不能从根节点沿着指针追踪得到,所以他会一直是白色,最后被回收。
着色操作能得以实现归功于每个内存块中叫做gcmarkBits
的位,这个位用来将跟踪扫描过的地方设成1:
如我们所见,黑色与灰色是同样的工作方式。在处理上不同的地方是,灰色是可以被入队扫描的,而黑色是指向链的尾部。
以上步骤完成以后,垃圾回收器会启动Stop the world,启用写屏障,将期间的内存改变情况全部入队垃圾回收器工作池,然后将这些入队的内存重复以上的步骤进行标记。
运行时分析器 Runtime profiler
这是一个由Go提供的工具,允许我们可视化每一步垃圾回收的过程,并看到垃圾回收是对我们程序的影响有多大。使用这个跟踪工具运行我们项目代码能够还能提供强大的可视化结果,下面是跟踪图
标记线程的生命周期同样可以以goroutinue级别进行可视化。这是goroutine#33的示例,它在开始标记内存之前先在后台等待。
有疑问加站长微信联系(非本文作者)