2022-03-21 Go垃圾回收之旅6 - ROC与Write Barrier
今天,我们来看GC的一种设计 - ROC(Request Oriented Collector)。虽然ROC并没有被实际工程采用,但很值得我们学习,加深理解。
《Go垃圾回收之旅》原文链接 - https://go.dev/blog/ismmkeynote
ROC-面向请求的回收器
ROC提出了一种假设:
Objects associated with a completed request or a dormant goroutine die at a higher rate than other object.
与一个完整请求 或 休眠 goroutine 所关联的对象们`,比其它对象更容易死亡。
我们假设存在两个Goroutine - G1和G2,它们的对象分为如下三类:
- G1私有
- G2私有
- G1和G2共有
当G1的生命周期结束时,即Goroutine退出,G1私有的的对象就应该被回收,这一点很容易理解。
但是,程序实际运行的过程中,对象一直在变化,也就是G1私有的对象变成了G1和G2共有的。这个时候,我们就必须引入一个新的概念 - write barrier。
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个思路:
- 每次删除指针引用时,看一下这个对象、是否只有一个Goroutine的引用,是的话转为私有
- 不处理。等这个对象没有任何引用时,用GC清理
小结
ROC的思想很朴素,非常符合我们的直觉,具有一定的参考价值。
而写屏障目前被广泛地应用在各类GC中,今天我们也借ROC对它有了初步印象。
2022-03-24 Go1.5的GC概览1 - 官方Talk
在上一个系列,我们通过阅读 Go垃圾回收之旅 的相关资料,对Go中GC的很多概念有了基本的认识,这就给我们接下来的学习铺好了路。
今天开始,我们将一起阅读下一篇内容,也就是官方博客对Go1.5版本GC的讲解。
原文链接 - https://go.dev/blog/go15gc
为什么我不选择最新版本进行讲解呢?
Go1.5的GC实现是具有一定里程碑意义的,实现了 并发标记清扫,与最新的GC实现差异并不大,作为入门学习资料更容易理解。
在这篇博客中,作者先引入了一个Talk,里面重点讲述了GC的实现与性能,而实现部分是我们今天的重点。
GC相关的差异(Go与Java)
维度 | GO | java |
---|---|---|
运行线程 | 1000+Goroutine | 10+线程 |
同步机制 | channel | 锁 |
运行时实现 | Go语言实现 | C语言实现 |
内存分布 | 具备局部性 | 通过指针跳转 |
GC概览
- Scan Phase 扫描
- Mark Phase 标记
- Sweep Phase 清理
关于这三个阶段是怎么实现的,可以对照着ppt看,或者观看视频 - https://www.bilibili.com/video/BV18r4y1q7p3
关于更细节的 GC Algorithm Phases 实现,我们会在下一讲描述。
小结
本篇内容主要结合这个Talk,讲述了Go1.5版本的GC基本实现,希望大家能对GC背景和三阶段操作有基本了解。
2022-03-26 Go1.5的GC概览2 - GC Algorithm Phases
在上一篇,我们从这篇Talk - https://go.dev/talks/2015/go-gc.pdf 里了解标记清理算法。
今天,我们将对着下面这张Go1.5 GC算法的各个阶段,串讲一下GC这个过程。
Stack scan 栈扫描
栈扫描的启动阶段有一小段STW,这是因为GC要启动写屏障,所以必须先暂停所有Goroutine的运行。这个时间很短,大概耗时在几十微秒。
runtime中的写屏障的数据结构如下:
1 | var writeBarrier struct { |
完成启动后,就进入这一步的工作:从全局变量和各个Goroutine的栈上收集指针信息。这一步,也就是初始化所有标记对象的集合。
Mark 标记
标记阶段即根据扫描出的初始指针对象,做BFS遍历,也就将所有可触达的对象加上标记。这里有一句话:
Write barrier tracks pointer changes by mutator. 也就是在标记阶段中,如果有程序变更了指针,就需要添加写屏障。
关于写屏障的实现细节我们先不细聊,先一起来看看GC中的三个概念:
- mutator:一般指应用程序,在运行过程中,会不停地修改堆对象里的指向关系
- collector:垃圾回收期,更多地是指GC线程
- allocator:内存分配器,也就是程序向操作系统申请内存、释放内存,这一点在GC里很重要,往往被我们忽视
Mark Termination 标记结束
完成标记后,主要分为三个工作:
- Rescan - 重新扫描其中变化的内容
- Clean Up Tasks - 这里的清理并不是清理对象,而是对整个Mark标记的收尾工作,比如收缩栈
- 关闭写屏障
注意,这一整个阶段都是STW的。
Sweep 清扫
Sweep就是将未标记的堆上对象进行清理,回收资源。这一阶段是并发的。
值得一提的是,我们之前谈论过的GC Paging算法就是在这一步启动的,用在估算下一次启动GC的最佳时间。
Github: https://github.com/Junedayday/code_reading
Blog: http://junes.tech/
Bilibili: https://space.bilibili.com/293775192
公众号: golangcoding