golang的垃圾回收是对基于可达性分析思想的标记清除算法进行改进而来的。通过并发标记和增量收集,可以有效减少Stop The World的时间。
可达性分析回收(追踪式回收算法)
可达性分析回收算法的核心思想是判断一个对象是否可达,如果这个对象一旦不可达就可以立刻被GC回收了。
那么我们怎么判断一个对象是否可达呢?第一步从根节点开始找出所有的全局变量和当前函数栈里的变量,标记为可达。第二步,从已经标记的数据开始,进一步标记它们可访问的变量,以此类推,专业术语叫传递闭包。当追踪结束时,没有被打上标记的对象就被判定是不可触达。
三色标记(堆对象)
三色标记法,将堆上对象通过颜色划分为三类:已确认作活跃对象标记为黑色,待确认对象标记为灰色,未访问对象标记为白色。三色标记法就是首先将根对象(全局变量、栈以及goroutine栈上变量、寄存器变量)指向的堆上对象标记为灰色,然后并发遍历这些灰色对象,将其标记为黑色,将其引用的对象也标记为灰色;如此直至没有灰色对象,剩下的白色对象就是可以回收的。
- 初始标记阶段(Initial Marking):在这个阶段,垃圾回收器从根对象开始,标记所有根对象引用的对象为灰色,将栈上所有可达对象标记为黑色(since go1.8),根对象可以是全局变量、栈中的变量等。这个阶段需要STW(Stop-The-World)。
- 并发标记阶段(Concurrent Marking):在初始标记阶段完成后,垃圾回收器会进入并发标记阶段,在这个阶段,垃圾回收器会并发地遍历堆中的对象,将灰色对象引用的对象标记为灰色,并将自己标记为黑色,然后将自己从灰色队列中移除。这个阶段可以与程序的执行并发进行,不需要STW。
- 清扫阶段(Sweeping):在并发标记完成后,垃圾回收器会进行清扫阶段,清扫所有未标记的白色对象,并将它们回收。这个阶段也可以与程序的执行并发进行,不需要STW。
堆上的混合写屏障
在三色标记过程中,如果不存在STW,假设发生这样一种情况:
obj1已经扫描过标记为黑色;obj2即将被扫描标记为灰色,obj3待扫描标记为白色。此时执行业务的goroutine断开了灰色对象obj2到白色对象obj3的引用,并创建了黑色对象obj1到obj3的引用,由于obj3仅有obj1的引用且obj1为黑色不会再扫描,obj1会一直维持白色,被垃圾回收掉。但是obj3显然应被视为活跃对象,其对执行中的业务是可见的,如果业务再次对其进行访问,将会引发panic。
为了解决这个问题,golang的垃圾回收引入了混合写屏障技术。
混合写屏障是为了解决在并发标记过程中,当一个goroutine在标记对象时,另一个goroutine可能会并发地修改这些对象的指针的情况。混合写屏障通过在写入指针时进行特殊处理(修改指针前对对象进行标记),确保在并发标记过程中不会错过任何需要标记的对象,并保证标记的一致性。
插入写屏障
当一个对象引用另外一个对象时,将另外一个对象标记为灰色,以此满足强三色不变性,不会存在黑色对象引用白色对象。
删除写屏障
在灰色对象删除对白色对象的引用时,将白色对象置为灰色,其实就是快照保存旧的引用关系,这叫STAB(snapshot-at-the-beginning),以此满足弱三色不变性。
混合写屏障(go1.8开始)
- GC开始将栈上可达的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW)
- GC期间,任何在栈上创建的新对象,均为黑色
- 被删除的对象标记为灰色(触发写屏障)
- 被添加的对象标记为灰色(触发写屏障)
辅助gc
为了防止应用程序申请内存的速度比Mark的速度还要快的情况,Go采用了一种叫辅助GC也叫gcAssist的方法,它遵循一条非常简单原则,分配多少内存就需要完成多少标记任务,简单说就是在Mark阶段开始以后,就给内存的申请者提了一个要求,在给你分配内存之前,你得先帮我标记与之相当的内存出来,让应用程序辅助完成标记操作。
gc的时机
- 内存分配量达到阈值:每次内存分配都会检查当前内存分配量是否达到阈值,如果达到阈值则触发GC。阈值 = 上次 GC 内存分配量 * 内存增长率,
内存增长率由环境变量 GOGC 控制,默认为100,即每当内存扩大一倍时启动GC。 - 定时触发GC:
默认情况下,2分钟触发一次GC,该间隔由src/runtime/proc.go的forcegcperiod 声明。 - 手动触发GC:
在代码中,可通过使用 runtime.GC() 手动触发GC。
STW
1.8开始gc只有在标记开始(打开写屏障和辅助gc)以及标记结束(关闭写屏障和辅助gc)的时候需要短暂的STW。