一、go的垃圾回收算法
说句实话,对go的垃圾回收算法不太熟悉,正好借这个机会学习一下。在Go语言中1.1是使用的STW,1.3是使用的标记删除算法,1.5采用的是三色标记算法(其实没啥大的改动,正如前面分析),1.8之后采用的是三色标记+混全写屏障机制。从整体划分来看,Go的GC的算法仍然是属于标记-清除算法。
一如以前所分析,所有的厂商在实现自己的算法时,一定会融入各种合适的其它辅助算法或者技术手段来提高相关的场景适应能力。即使如此,个人经验在国内一些大厂中的应用里,高内存和高并发使用时,go仍然出现了GC停止过长的问题。这个不是本文讨论的问题,有兴趣可以到网上搜索一番。
二、GC源代码结构
下载一个三色标记的1.8版本找到相关代码,其结构如下:
malloc.go:基本就是内存的分配管理
mgc.go:主要的GC代码和流程
mgcmark.go:扫描标记阶段代码
mgcsweep.go:清除代码
mgcsweepbuf.go:mspan的集合及相关数据结构代码
mgcwork.go:GC抽象工作池代码,包含扫描标记的队列相关及写屏障的操作
GC的主要代码就是在mgc.go中,在这个源码上有整个GC的说明,包括对不同对象,GC的效率速度等都有
三、总体流程
在这个版本里,其主要流程如下:
-
GC执行扫描终止。
a.停止世界(这个说起来一直非常别扭)。这会使所有Ps(ParallelScavenge)达到GC安全点。
b.扫描所有Spans。只有在强制GC时才会进行。 -
GC执行“标记1”子阶段。在此子阶段中,Ps为允许本地缓存部分工作队列。
a.通过将gc phase设置为_GCmark,为标记阶段做好准备(从_GCoff),启用写屏障,启用mutator协助根标记作业并将其排入队列。任何对象都不能扫描,直到所有Ps都启用了写屏障,即STW完成。
b.启动世界。即GC工作由mark进程完成由调度员和助理启动的分配的一部分。写入障碍使覆盖的指针和任何指针的新指针值写入(有关详细信息,请参阅mbarrier.go)。新分配的对象立即标记为黑色。
c.GC执行根标记作业。这包括扫描所有堆栈,着色所有全局变量,着色中的所有堆指针堆外运行时数据结构。扫描堆栈会停止goroutine,对堆栈上找到的任何指针进行着色,然后继续goroutine。
d.GC推出灰色对象的工作队列,扫描每个灰色对象(及其子对象),将对象变黑,并对对象中找到的所有指针着色(这反过来可能会将这些指针添加到工作队列)。 -
一旦全局工作队列为空(但本地工作队列缓存可能仍包含工作),GC执行“标记2”子阶段。
a.GC停止所有工作线程,禁用本地工作队列缓存,将每个P’s的本地工作队列缓存刷新到全局工作队列缓存,并重新启用工作者。
b.GC再次弹出工作队列,如上面2d项目所示。 -
一旦工作队列为空,GC将执行标记终止。
a.停止世界。
b.将gc phase设置为_GCmarktermination,并禁用工作进程和助手进程。
c.从工作队列中弹出任何剩余工作(通常在那里将为空)。
d.执行其他事务管理,如更新mcaches。 -
GC执行扫描阶段。
a.通过将gc phase设置为_GCoff,为扫描阶段做好准备,设置扫描状态并禁用写屏障。
b.启动世界。从现在起,新分配的对象默认为白色,如有必要,请在使用前分配spans。
c.GC在后台和响应中执行并行扫描分配。请参见下面的说明。 -
当发生足够的分配时,重置序列从上面的1开始。
为了提高效率,Go还提供了GC的并行扫描清除算法,并加强了对小对象的处理。
四、总结
golang的垃圾回收代码也是一个发展迭代的过程,这个可以在各种资料和源码中体现出来,其实看源码是最好理解GC的原理的方法,特别是有针对性对不同的语言中的GC机制和源码进行对比分析,就会有更好的收获以及对GC更深入的理解和掌握。能不能跑得快就得先走得好。能不能创新,就得先看看别人怎么做。别人没做过,你做了,就是创新。而更好的是,别人的工作很可能诱导出你的奇思妙想。