2020-03
今天,我们来看GC的一种设计 - ROC(Request Oriented Collector)。虽然ROC并没有被实际工程采用,但很值得我们学习,加深理解。
《Go垃圾回收之旅》原文链接 - https://go.dev/blog/ismmkeynote
ROC提出了一种假设:
Objects associated with a completed request or a dormant goroutine die at a higher rate than other object.
与一个完整请求 或 休眠 goroutine 所关联的对象们`,比其它对象更容易死亡。
我们假设存在两个Goroutine - G1和G2,它们的对象分为如下三类:
当G1的生命周期结束时,即Goroutine退出,G1私有的的对象就应该被回收,这一点很容易理解。
但是,程序实际运行的过程中,对象一直在变化,也就是G1私有的对象变成了G1和G2共有的。这个时候,我们就必须引入一个新的概念 - write barrier。
我们通过一句话来了解的写屏障功能:
Whenever there was a write, we would have to see if it was writing a pointer to a private object into a public object.
也就是说,当有个写请求时,我们就必须检查它是否将一个指针从私有对象变成了公共对象。这里注意两个点:
由于第一点的存在,ROC需要始终开启写屏障,给整个程序带来了大量的成本,所以ROC最终没有被采用。
我们不妨延伸地思考一下,当一个共有对象变成私有时,该怎么操作?我这边提供2个思路:
ROC的思想很朴素,非常符合我们的直觉,具有一定的参考价值。
而写屏障目前被广泛地应用在各类GC中,今天我们也借ROC对它有了初步印象。
在上一个系列,我们通过阅读 Go垃圾回收之旅 的相关资料,对Go中GC的很多概念有了基本的认识,这就给我们接下来的学习铺好了路。
今天开始,我们将一起阅读下一篇内容,也就是官方博客对Go1.5版本GC的讲解。
原文链接 - https://go.dev/blog/go15gc 为什么我不选择最新版本进行讲解呢? Go1.5的GC实现是具有一定里程碑意义的,实现了 并发标记清扫,与最新的GC实现差异并不大,作为入门学习资料更容易理解。
在这篇博客中,作者先引入了一个Talk,里面重点讲述了GC的实现与性能,而实现部分是我们今天的重点。
请跳转阅读 - https://go.dev/talks/2015/go-gc.pdf
维度 | GO | java |
---|---|---|
运行线程 | 1000+Goroutine | 10+线程 |
同步机制 | channel | 锁 |
运行时实现 | Go语言实现 | C语言实现 |
内存分布 | 具备局部性 | 通过指针跳转 |
关于这三个阶段是怎么实现的,可以对照着ppt看,或者观看视频 - https://www.bilibili.com/video/BV18r4y1q7p3
关于更细节的 GC Algorithm Phases 实现,我们会在下一讲描述。
本篇内容主要结合这个Talk,讲述了Go1.5版本的GC基本实现,希望大家能对GC背景和三阶段操作有基本了解。
在上一篇,我们从这篇Talk - https://go.dev/talks/2015/go-gc.pdf 里了解标记清理算法。
今天,我们将对着下面这张Go1.5 GC算法的各个阶段,串讲一下GC这个过程。
Go 1.5 GC
栈扫描的启动阶段有一小段STW,这是因为GC要启动写屏障,所以必须先暂停所有Goroutine的运行。这个时间很短,大概耗时在几十微秒。
runtime中的写屏障的数据结构如下:
var writeBarrier struct {
enabled bool // compiler emits a check of this before calling write barrier
pad [3]byte // compiler uses 32-bit load for "enabled" field
needed bool // whether we need a write barrier for current GC phase
cgo bool // whether we need a write barrier for a cgo check
alignme uint64 // guarantee alignment so that compiler can use a 32 or 64-bit load
}
完成启动后,就进入这一步的工作:从全局变量和各个Goroutine的栈上收集指针信息。这一步,也就是初始化所有标记对象的集合。
标记阶段即根据扫描出的初始指针对象,做BFS遍历,也就将所有可触达的对象加上标记。这里有一句话:
Write barrier tracks pointer changes by mutator. 也就是在标记阶段中,如果有程序变更了指针,就需要添加写屏障。
关于写屏障的实现细节我们先不细聊,先一起来看看GC中的三个概念:
完成标记后,主要分为三个工作:
注意,这一整个阶段都是STW的。
Sweep就是将未标记的堆上对象进行清理,回收资源。这一阶段是并发的。
值得一提的是,我们之前谈论过的GC Paging算法就是在这一步启动的,用在估算下一次启动GC的最佳时间。
Github: https://github.com/Junedayday/code_reading Blog: http://junes.tech/ Bilibili: https://space.bilibili.com/293775192