Junedayday Blog

六月天天的个人博客

0%

五分钟技术小分享 - 2022Week12

2020-03

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.

也就是说,当有个写请求时,我们就必须检查它是否将一个指针从私有对象变成了公共对象。这里注意两个点:

  1. 对象的复杂性 - 如果一个对象从私有变成共有,那么它内部的子对象也需要变化
  2. 针对指针 - 不用考虑一些值拷贝的对象

由于第一点的存在,ROC需要始终开启写屏障,给整个程序带来了大量的成本,所以ROC最终没有被采用。

我们不妨延伸地思考一下,当一个共有对象变成私有时,该怎么操作?我这边提供2个思路:

  1. 每次删除指针引用时,看一下这个对象、是否只有一个Goroutine的引用,是的话转为私有
  2. 不处理。等这个对象没有任何引用时,用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的实现与性能,而实现部分使我们今天的重点。

请跳转阅读 - https://go.dev/talks/2015/go-gc.pdf

GC相关的差异(Go与Java)

维度 GO java
运行线程 1000+Goroutine 10+线程
同步机制 channel
运行时实现 Go语言实现 C语言实现
内存分布 具备局部性 通过指针跳转

GC概览

  1. Scan Phase 扫描
  2. Mark Phase 标记
  3. 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这个过程。

Go 1.5 GC

Stack scan 栈扫描

栈扫描的启动阶段有一小段STW,这是因为GC要启动写屏障,所以必须先暂停所有Goroutine的运行。这个时间很短,大概耗时在几十微秒。

runtime中的写屏障的数据结构如下:

1
2
3
4
5
6
7
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的栈上收集指针信息。这一步,也就是初始化所有标记对象的集合。

Mark 标记

标记阶段即根据扫描出的初始指针对象,做BFS遍历,也就将所有可触达的对象加上标记。这里有一句话:

Write barrier tracks pointer changes by mutator. 也就是在标记阶段中,如果有程序变更了指针,就需要添加写屏障。

关于写屏障的实现细节我们先不细聊,先一起来看看GC中的三个概念:

  1. mutator:一般指应用程序,在运行过程中,会不停地修改堆对象里的指向关系
  2. collector:垃圾回收期,更多地是指GC线程
  3. allocator:内存分配器,也就是程序向操作系统申请内存、释放内存,这一点在GC里很重要,往往被我们忽视

Mark Termination 标记结束

完成标记后,主要分为三个工作:

  1. Rescan - 重新扫描其中变化的内容
  2. Clean Up Tasks - 这里的清理并不是清理对象,而是对整个Mark标记的收尾工作,比如收缩栈
  3. 关闭写屏障

注意,这一整个阶段都是STW的。

Sweep 清扫

Sweep就是将未标记的堆上对象进行清理,回收资源。这一阶段是并发的。

值得一提的是,我们之前谈论过的GC Paging算法就是在这一步启动的,用在估算下一次启动GC的最佳时间。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili: https://space.bilibili.com/293775192

公众号: golangcoding

二维码