一文搞懂Go语言垃圾回收机制
一文搞懂Go语言垃圾回收机制
Go语言的垃圾回收机制是其核心特性之一,通过自动化管理内存,极大地简化了开发者的负担。本文将深入探讨Go语言垃圾回收器的工作原理,从基本的标记-清除算法到先进的三色标记法,再到具体的屏障技术实现,帮助读者全面理解这一复杂而精妙的系统。
垃圾回收器(GC)的概述
垃圾回收器(GC)是一种自动化的内存管理机制,负责检测并回收不再使用的内存资源。Go语言中的垃圾回收器是一种具有并发特性的垃圾回收器,它可以在不阻塞程序执行的情况下回收不再使用的内存。这种并发的垃圾回收机制是Go语言的一大特点,使得开发者能够编写高并发和高性能的程序。
标记-清除(Mark and Sweep)算法
Go语言中的垃圾回收器使用了标记清除(Mark and Sweep)算法,其执行过程可以分成标记阶段(Mark)和清除(Sweep)两个阶段:
- 标记阶段:从根节点开始遍历所有可达对象,并将其标记为活动对象。
- 清除阶段:清除未被标记的对象,并回收它们占用的内存空间。
然而,原始的标记-清除算法存在一个严重的问题:在整个GC期间需要STW(Stop the World),即暂停所有程序的执行,直到垃圾回收完成。这会导致程序出现明显的延迟,影响用户体验。
为了解决这个问题,Go v1.5版本引入了三色标记法。
三色标记法
三色标记法通过增量标记和并发标记,减少了GC的停顿时间,提高了程序的相应性能。三色只是为了叙述上方便抽象出来的一种说法,实际上对象并没有颜色之分,这里的三色对应的是垃圾回收过程中对象的三种状态:
- 白色对象:表示对象尚未被扫描。所有的新对象都是白色的。
- 灰色对象:表示对象已被扫描,但其引用的其他对象尚未扫描。灰色对象将被进一步扫描。
- 黑色对象:表示对象已被扫描,并且其引用的所有对象也已扫描。黑色对象是安全的,不会被回收。
三色标记垃圾收集器的执行过程
- 初始状态:所有对象初始状态为白色,表示未被访问或未被标记。
- 根节点标记:从根节点开始,将其标记为灰色,并将其放入待处理的灰色对象队列中。
- 标记阶段:
- 从灰色对象队列中选择一个灰色对象将其标记成黑色,表示已被访问和标记为活动对象。
- 遍历该对象引用的其他对象,对于遇到的白色对象,将其标记为灰色,并放入灰色地向队列中等待处理。
- 如果灰色对象对列为空,则表示所有可达对象都已被标记为活动对象,标记阶段结束。
- 如果灰色对象队列非空,则继续从队列中取出下一个灰色对象进行处理,直到对列为空。
- 在标记阶段中,对象的引用关系会被遍历和追踪,从而确定哪些对象是可达的、活动的。
- 清除阶段:在标记阶段结束后,所有未被标记为黑色的白色对象被判定为垃圾对象进行回收。
然而,三色标记法也存在一个潜在的问题:并发标记可能会导致漏标记。
三色标记法漏标记问题
在并发标记过程中,用户程序可能会修改对象的引用关系,导致本应该被标记的对象没有被标记,从而被错误回收。
为了解决这个问题,Go语言引入了屏障技术。
屏障技术
屏障技术是在并发垃圾回收中用于解决并发写如问题的一种常见方法。他们通过拦截应用程序的读取和写入操作,在操作中进行必要的处理,以确保垃圾回收器能够正确的跟踪对象引用关系。
想要在并发标记或者增量标记算法中保证正确性,我们需要达成以下两种三色不变性中的一种:
- 强三色不变性:黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象。
- 弱三色不变性:黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径。
遵循上述两个不变性中的任意一种,我们都能保证垃圾回收算法的一致性和正确性,而屏障技术就是在并发或者增量标记过程中保证三色不变性的重要技术。
垃圾收集中的屏障技术可以被视为一种钩子方法,它在用户程序进行对象读取、创建对象和更新对象指针时执行特定的代码段。根据操作类型的不同,我们将这些屏障技术分为读屏障和写屏障两种。由于读屏障需要在读操作中插入代码片段,地拥护程序的性能影响较大,因此编程语言通常采用写屏障来确保三色不变性。
接下来我们要介绍的是Go语言中采用的两种写屏障技术,分别是Dijkstra所提出的插入写屏障和Yuasa提出的删除写屏障,这里会分析他们如何保证三色不变性和垃圾收集器的正确性。
插入写屏障
Go语言中的插入写屏障技术,主要是用于在对象被更新时标记相关的引用。它的主要原理是在写操作发生时,通过插入额外的代码来确保垃圾收集器能够正确的跟踪对象之间的引用关系。
具体而言,当一个对象被写入新的数值时,插入写屏障会执行以下步骤:
- 检查被写入的对象是否属于堆(heap)上的可达对象。如果不是可达对象,即将被当做垃圾回收,无需执行额外操作。
- 如果被写入的对象是堆上的可达对象,那么插入写屏障将会进行以下操作:
- 标记被写入的对象为灰色,表示该对象需要被垃圾收集器处理。
- 检查被写入的数值是否为一个指针。如果是一个指针,则进一步处理。
- 对于指针类型的数值,检查指向的对象是否已经被标记为黑色或灰色。如果指向的对象尚未被标记,插入写屏障将标记该对象为灰色,确保垃圾收集器可以遍历到该对象。
以下是一个简单的示例代码,演示了插入写屏障的原理:
type Object struct {
value int
next *Object
}
func main() {
obj1 := &Object{value: 1}
obj2 := &Object{value: 2}
// 执行写操作,插入写屏障生效
obj1.next = obj2
}
在上述代码中,当obj1.next
被赋予新的值obj2
时,插入写屏障会自动触发。它将会标记obj2
为灰色,确保垃圾收集器能够正确地跟踪到obj2
的引用关系,并在垃圾收集阶段进行适当的处理。
删除写屏障
删除写屏障的规则是在删除引用是,如果被删除的引用对象自身为灰色或者白色,那么被标记为灰色。
下面将通过4张图展示删除写屏障的执行过程:
混合写屏障
插入写屏障和删除写屏障机制虽然均可保护对象不被漏标记而错误回收被引用的对象,但是都还是有各自缺点。在Go语言v1.5版本中使用了插入写屏障机制,STW的效率还是不高,因为它需要在标记结束时STW来重新扫描栈,标记栈上引用的白色对象的存活,这个过程会增加STW时间。
于是在Go v1.8版本,通过结合插入写屏障和删除屏障的混合写屏障机制来提高STW效率,缩短STW时间。混合写屏障机制避免了对栈re-scan的需要,实现了黑色赋值器,不用STW扫描栈。混合写屏障机制也消除了所有的STW,实现了近乎实时垃圾回收。
总结
垃圾回收是一种自动内存管理机制,他可已自动地检测和施放不再使用的内存。Go语言在垃圾回收方面有着很好的表现,它采用了三色标记法和混合写屏障机制来实现垃圾回收。三色标记法可以有效地避免内存泄漏和误回收,而混合写屏障机制则可以减少STW时间,提高垃圾回收的效率。Go语言的垃圾回收机制具有高效、安全、自动化等优点,可以大大提高程序的稳定性和可靠性。
Go语言垃圾回收集器的演进过程:
- Go v1.3之前采用了传统的标记-清除法,需要STW,会暂停整个程序的运行。
- Go v1.5中引入了三色标记法和插入写屏障机制,大幅降低垃圾收集的延迟(从几百ms降低至10ms以下)。
- Go v1.8中引入了混合写屏障机制(结合了删除写屏障机制),将垃圾收集的时间降低至0.5ms以内。