2022-03-28 Go1.5的GC概览3 - Tri-color
在标记阶段,Go语言使用了 tri-color,也就是著名的三色标记法。在这篇文章里,详细地描述了这部分的实现。
原文链接 - https://go.dev/blog/go15gc
三色标记法是一种堆上对象的图算法。这里图的边Edge即指针,所以这里的关系是单向的。
In a tri-color collector, every object is either white, grey, or black and we view the heap as a graph of connected objects.
接下来,就是具体的三色标记法的工作了:
At the start of a GC cycle all objects are white. The GC visits all roots, which are objects directly accessible by the application such as globals and things on the stack, and colors these grey. The GC then chooses a grey object, blackens it, and then scans it for pointers to other objects. When this scan finds a pointer to a white object, it turns that object grey. This process repeats until there are no more grey objects. At this point, white objects are known to be unreachable and can be reused.
这一段内容很长,但描述得很直白,我简单概括下:
- GC初始化
- 将所有的对象设置为 白色
- Mark的初始化
- 将全局变量和栈上的对象,标记为 灰色
- 这些灰色对象会被放入队列中
- Mark的核心流程
- 从队列中弹出一个灰色对象
- 访问这个灰色对象的指针,将白色对象的转变为灰色对象,并加入到队列中
- 将这个灰色对象标记为黑色,表示访问完毕
- 重复上述过程,直到队列为空
- 清理阶段
- 将所有剩余的 白色对象 进行垃圾回收
我们重点看这里的 Mark的核心流程,里面有个关键问题:mutator
(也就是运行中的程序)在不停地修改对象的指针,所以会出现各种异常情况,比如说让一个黑色对象指向白色对象(正常情况下,黑色对象指向的是黑色或者灰色)。
网上有很多关于三色标记的资料,不太清楚的朋友需要自行搜索,比如 https://segmentfault.com/a/1190000022030353 。
重点可以结合写屏障要解决的问题,进行理解。
这个时候,就引入了我们前面说的内容 - 写屏障write barrier。
Go’s write barrier colors the now-reachable object grey if it is currently white, ensuring that the garbage collector will eventually scan it for pointers.
写屏障即会在每次发生指针变更时,加入一小段代码:比如检测到新的被指向的对象是白色,就将它修改为灰色,需要扫描。这只是一个简单例子,后续Go语言对写屏障进行了迭代,采用的写屏障技术是 混合写屏障,也就是 插入写屏障+删除写屏障。
2022-04-02 Go1.5的GC概览4 - 深入细节
今天我们会开始抠细节,来加深大家对这块的理解。
原文链接 - https://go.dev/blog/go15gc
Of course the devil is in the details. 细节才是恶魔,但只有去抠这些细节,我们才能掌握GC的实现。作者在文中抛出了很多问题,我们挑出3个关键性的问题进行回答。
When do we start a GC cycle?
一个主动调用和两个被动调用。
主动调用指的是代码调用runtime.GC()
函数,被动调用包括 周期性强制执行(如2min)和GC Pacing算法。
其中GC Pacing主要和堆上空间的增长速度相关,增长越快,GC频率越快。
How do we know where the roots are?
标记阶段的根节点来自于哪里呢?从程序的维度来说,包括 全局变量 和 Goroutine的栈上变量。
全局变量,对应到进程结构中的bss段(未初始化的全局变量)和data段(已初始化的全局变量)。bss和data段的概念是通用的,也就是Go、C++等任意进程都是这样的数据结构。
而Goroutine栈上变量是Go的runtime自己维护的。
How do we know where in an object pointers are located?
在一个对象中,我们如何识别出对象内部的指针呢?
首先,我们要了解到,一个对象在内存中是一段连续0和1。由于Go语言的强类型特点,我们可以清楚地计算出这个结构体的总大小、以及内部各个成员变量的大小。
对象内部的变量分为两类:具体的数值和指针。具体的数值,如int64 a=1,那就在开辟一个可以存储int64大小空间段,存下1这个数值;而指针呢,如*int64这个指针,它在堆上先存储int64具体的值,记录它的起始地址如0x1111,结构体内部开辟一个指针大小的空间,记录地址的值0x1111。
如果抛开强类型,我们看到程序中的数值是无法区分的,比如上述例子中的1可以被理解地址0x0001,而0x1111也可以被理解为是具体的数字。强类型的语言是一种解决方案,它能在编译期就识别出具体的类型。
为了延伸思考,这边也提一下另一种方案:
扩展数据,将它拆分为 对象类型(如前x位)+数据 。比如,
- 数值1 = int64数据 + 1
- 指针1 = int64的指针 + 指针起始地址
这就有JAVA里“一切皆对象”的影子了。
小结
细节既是恶魔,也提供了我们梳理知识体系的过程。虽然大多数的时间我们没法掌握细节,但只要怀着一颗保持探索的心,总是能不断往前进的。
Github: https://github.com/Junedayday/code_reading
Blog: http://junes.tech/
Bilibili: https://space.bilibili.com/293775192
公众号: golangcoding