golang 垃圾收集机制
背景
垃圾回收(Garbage Collection,GC) 是Go语言的核心特性之一,是实现内存自动管理的一种形式。golang的自动垃圾回收屏蔽了复杂且容易出错的内存操作,让开发变得更加简单、高效。在Go语言中,从实现机制上来说,垃圾回收可能是最复杂的模块了。了解垃圾回收的机制,有助于更好地理解Go语言的内存管理机制,从而更好的使用Go语言进行开发。
定义
- **STW(Stop The World):**指的是在垃圾回收期间,Go 会暂停所有正在运行的程序 Goroutine,以便安全地执行内存清理操作。STW 会让所有业务逻辑短暂停止运行,导致请求处理变慢甚至出现延迟抖动,特别是在高并发或对响应时间要求严格的场景中会影响用户体验。
- **写屏障(Write Barrier):**这个概念乍一听有点硬核,但其实可以类比成一个"门卫"或者"报警器",它的作用就是:在内存被修改(写入)时,拦一下,顺便记录或执行一些操作,以保证垃圾回收的正确性。
- 根对象:
- 全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
- 执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。
- 寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。
垃圾收集
常见的 GC 算法
引用计数法
根据对象自身的引用计数来回收,当引用计数归零时进行回收,但是计数频繁更新会带来更多开销,且无法解决循环引用的问题。
- 优点:简单直接,回收速度快
- 缺点:需要额外的空间存放计数,无法处理循环引用的情况;
标记清除法
标记出所有不需要回收的对象,在标记完成后统一回收掉所有未被标记的对象。
- 优点:简单直接,速度快,适合可回收对象不多的场景
- 缺点:会造成不连续的内存空间(内存碎片),导致有大的对象创建的时候,明明内存中总内存是够的,但是空间不是连续的造成对象无法分配;
复制法
复制法将内存分为大小相同的两块,每次使用其中的一块,当这一块的内存使用完后,将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉
- 优点:解决了内存碎片的问题,每次清除针对的都是整块内存,但是因为移动对象需要耗费时间,效率低于标记清除法;
- 缺点:有部分内存总是利用不到,资源浪费,移动存活对象比较耗时,并且如果存活对象较多的时候,需要担保机制确保复制区有足够的空间可完成复制;
标记整理法
标记过程同标记清除法,结束后将存活对象压缩至一端,然后清除边界外的内容
- 优点:解决了内存碎片的问题,也不像标记复制法那样需要担保机制,存活对象较多的场景也使适用;
- 缺点:性能低,因为在移动对象的时候不仅需要移动对象还要维护对象的引用地址,可能需要对内存经过几次扫描才能完成;
分代法
将对象根据存活时间的长短进行分类,存活时间小于某个值的为年轻代,存活时间大于某个值的为老年代,永远不会参与回收的对象为永久代。并根据分代假设(如果一个对象存活时间不长则倾向于被回收,如果一个对象已经存活很长时间则倾向于存活更长时间)对对象进行回收。
比较经典的就是 Java 中的分代 GC 了
Go 语言垃圾收集
Go 语言 GC 主要使用的是三色标记法的 GC 算法,会在下文中介绍到,Go 语言中 GC 时的 STW 的情况还是存在的,只是说在不断的迭代去尽量减少 STW 对程序运行产生的影响。
三色标记法
传统的标记清除算法中,垃圾收集器从垃圾收集的根对象出发,递归遍历这些对象指向的子对象并将所有可达的对象标记成存活;标记阶段结束后,垃圾收集器会依次遍历堆中的对象并清除其中的垃圾,整个过程需要标记对象的存活状态,用户程序在垃圾收集的过程中也不能执行,我们需要用到更复杂的机制来解决 STW 的问题,这就出现了三色标记法。
三色标记算法将程序中的对象分成白色、黑色和灰色三类:
- **白色对象(可能死亡):**未被回收器访问到的对象。在回收开始阶段,所有对象均为白色,当回收结束后,白色对象均不可达。
- **灰色对象(波面):**已被回收器访问到的对象,但回收器需要对其中的一个或多个指针进行扫描,因为他们可能还指向白色对象。
- **黑色对象(确定存活):**已被回收器访问到的对象,其中所有字段都已被扫描,黑色对象中任何一个指针都不可能直接指向白色对象。
标记过程
- 起初所有的对象都是白色的;
- 从根对象出发扫描所有可达对象,标记为灰色,放入待处理队列;
- 从待处理队列中取出灰色对象,将其引用的对象标记为灰色并放入待处理队列中,自身标记为黑色;
- 重复步骤(3),直到待处理队列为空,此时白色对象即为不可达的“垃圾”,回收白色对象;
并发问题
指的是在用户协程与 GC 协程并发执行的场景下,部分存活对象未被正确标记,从而被误删的情况。如图所示:
- 初始时刻: C 持有 D 的引用;
- GC 协程:A 被 GC 扫描完成,不会再次扫描,标记为黑色(此时 C 为灰色,还未完成扫描,D 为白色);
- 用户协程:A 建立对 D 的引用,C 删除对 D 的引用;
- GC 协程:开始扫描 C,C 被置为黑色,由于 C 删除了对 D 的引用,此时 D 未被扫描到,依然为白色,但其被 A 对象引用,不是该被回收的对象;
- GC 协程:标记完成,回收白色对象,D 被误删除。
漏标的情况不能容忍,因为删除了程序正在使用的对象,会造成程序异常。
为了解决上述并发的问题,后续就引入了写屏障一内容
写屏障
强弱三色不变式
-
强三色不变式
- 不存在黑色对象引用到白色对象的指针。
-
弱三色不变式
- 所有被黑色对象引用的白色对象都处于灰色保护状态.
Dijkstra 插入写屏障
- 插入写屏障:规则维护了强三色不变式,保证当一个黑色对象指向一个白色对象前,会先触发屏障,将白色对象置为灰色,再建立引用。
Yuasa 删除写屏障
- 删除写屏障:实现了弱三色不变式,保证当一个白色对象即将被上游删除引用前,会触发屏障将其置灰,之后再删除上游指向其的引用。但是引入删除写屏障,有一个弊端,就是一个白色对象的引用被删除后,置灰,即使没有其他存活的对象引用它,它仍然会活到下一轮。如此一来,会产生很多的冗余扫描成本,且降低了回收精度。
混合写屏障机制
在 Go 1.8 版本之前,栈上的对象在并发标记阶段可能会发生变化,导致需要在标记结束后进行一次 STW 操作并重新扫描栈上的对象,以确保所有存活的对象都被正确标记。然而,这个过程会增加垃圾收集的暂停时间,影响程序的性能。
在 Go 1.8 版本后,引入了混合写屏障机制(hybrid write barrier),避免了对栈 re-scan 的过程,极大的减少了 STW 的时间。混合写屏障的精度虽然比插入写屏障稍低,但它完全消除了对栈的 STW 重新扫描,从而进一步减少了 STW 的时间。
Go 1.8 版本后的 GC 混合写屏障有以下四个关键步骤:
- GC 刚开始的时候,会将栈上的可达对象全部标记为黑色。(在扫描栈帧的过程中,栈上的局部变量会被标记为黑色,因为它们被视为已处理的根对象,不需要再重新扫描)
- GC 期间,任何在栈上新创建的对象,均为黑色(避免在扫描过程中,重复扫描这些对象)。
- 堆上被删除的对象标记为灰色(删除写屏障)。
- 堆上新添加的对象标记为灰色(插入写屏障)。
在标记阶段一开始,所有栈上的对象都会被直接标记为黑色,后续任何在栈上新创建的对象,均为黑色。黑色对象表示已经访问且不需要再扫描其引用,由于栈上的对象已经标记为黑色,不再需要在标记结束后重新扫描栈上的对象,从而减少了 STW 的时间。
一次完整的 GC 流程
初始 STW 事件:
- 目的:暂停所有 Goroutines 后,内存状态到达一致性,是开启混合写屏障机制的前提,所以暂停所有 Goroutines 是必要的。
并发标记阶段:
- 开启混合写屏障机制:在初始 STW 事件阶段暂停所有 Goroutines 的同时,垃圾回收器会开启混合写屏障机制。(注意:混合写屏障机制主要是针对堆内存中的对象引用修改而设计的,它的主要目的是确保在垃圾回收过程中,任何新的对象引用修改都能被正确记录和处理)
- 为什么现在开始混合写屏障机制:暂停所有 Goroutines 后,内存状态到达一致性,一旦 Goroutines 恢复执行,内存状态一致性会被打破,无法得以保证;所以暂停所有 Goroutines 的主要目的是为了顺利开启混合写屏障机制,从而保障后续的内存状态一致性,这是确保垃圾回收过程有效和准确的关键步骤。
- 混合写屏障机制的作用:记录对象引用的修改,混合写屏障机制在开启后,任何新的对象引用修改都会被记录下来。
- 确保了在分批扫描 Goroutines 栈帧时,即使其他 Goroutines 被恢复运行并修改对象引用,这些修改也能被正确记录和处理。确保了在并发标记阶段,所有引用的修改会被正确记录和处理。
- 当对象引用被修改时,混合写屏障会记录这些修改,将新引用的对象标记为灰色(堆上被删除的对象标记为灰色,堆上新添加的对象标记为灰色)。
并发清除阶段:
一次完整的 GC 分为三个阶段:初始 STW 事件、并发标记阶段、并发清除阶段;随着 Go 版本的不断更迭,GC 的各项细节问题也得到了优化,优化方向包括:
- 增量式改进:通过增量标记和清理,进一步减少了每次 STW 暂停的时间。
- 优化内存管理:通过基于页的内存管理和减少内存碎片,提升了内存分配和释放的效率。
- 高效的数据结构:使用更高效的数据结构来维护垃圾回收状态,减少了开销。
总结
现代 Go 版本的垃圾收集器通过一系列优化措施,已经将 STW 时间减少到亚毫秒级别,极大地提高了程序的性能和响应性。未来版本可能会进一步优化垃圾收集器,以实现更短的 STW 时间。
经过上述介绍可能还并不全面,如果有想了解更细的同学可以自己去查看相关的资料。
引用
Golang垃圾回收(GC)介绍
Go 语言之垃圾回收机制
Golang 垃圾回收:一次 GC 周期的详细过程
[Go三关-典藏版]Golang垃圾回收+混合写屏障GC全分析