前言
go作为一个非常年轻的语言,吸取了各个语言的优点,比如说Java中优秀的垃圾回收,来释放程序员一部分精力。
本篇要说的就是垃圾回收,常见的垃圾回收算法有标记-清除、标记整理、复制,然后在这些算法基础上有分为分代&非分代回收,这些算法都非常优秀,只是面对的场景不同罢了,但是要是想透彻的理解垃圾回收,看Java中的实现再合适不过了,如果能对于Java中的垃圾回收非常熟悉,理解go的垃圾回收将非常简单。
go中的垃圾回收官方是这么描述的:非分代的、非紧缩的、写屏障的并发标记清除的垃圾回收。
标记清除
标记清除指的是对于那些已经不会再使用的对象进行标记,标记完成后,对于标记的对象进行清除。
image.png
很显然如果使用标记清除算法:
1、确定标记的起点GCRoot
2、存在一定的内存碎片
3、效率相对于复制、整理 效率要稍微高一些
但标记清除是最常见的垃圾回收算法,Java 中CMS等垃圾回收器用的就是这个。
在标记的过程中有一个所有垃圾回收算法(涉及GCRoot的初始标记)都有的问题stop-the-world
。
标记&清理的过程可以是串行的(效率很低),也可以是并发的。
三色标记
三色标记是一种在传统的标记清除算法基础上衍生出来的一个改进的并发标记算法: 1、首先创建三个集合:白、灰、黑 2、将所有对象放入白色集合中 3、然后从根节点开始遍历所有对象(注意这里并不递归遍历),把遍历到的对象从白色集合放入灰色集合。 4、之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合 5、重复 4 直到灰色中无任何对象 6、通过write-barrier检测对象有变化,重复以上操作 7、收集所有白色对象(垃圾)
image.png
非常关键的一点是GCRoot的确定,这是整个算法的开端: 当前goroutine的栈和全局数据区中的对象作为GCRoot。
三色标记中的并发标记
所谓的并发标记就是指在goroutine执行的过程中能进行标记行为,这里采用的方式与Java的CMS方式比较像,通过写屏障来保证正确性。 比如说:当从A这个GC root找到引用对象B时,B变灰A变黑。这时用户goroutine执行把A到B的引用改成了A到C的引用,同时B不再引用C。然后GC goroutine又执行,发现B没有引用对象,B变黑。而这时由于A已经变黑完成了扫描,C将当做白色不可达对象被清除,这里就会出现一个不该被清理的对象被清理了。 而写屏障就是在这个出错的地方做了下判断: 当发现A已经标记为黑色了,若A又引用C,那么把C变灰入队。go gc时借助一个队列,也就是gc-work来完成非递归遍历。
强制回收
因为系统启动或者短时间内大量分配对象这些原因,会将垃圾回收的gc_trigger(垃圾回收的触发器)的标准瞬间推高。当服务正常后,活跃对象远小于这个阈值,造成垃圾回收无法触发。 所以需要有一个强制回收的触发,sysmon每隔2分钟强制触发GC一次。强制GC的goroutine一直park在后台,直到sysmon将它唤醒开始执行gc。
GC整体过程
Goff to Gmark 每次的gcstart都是满足gc_triger时由mallocgc触发,整个的启动过程是stop the world的,这个过程启动了所有的GC工作协程,进入GCMark状态使能写屏障,启动gcController。简单来说就是确定GCroot相关的goroutine。 Gmark 这个阶段是标记阶段,拿到准备好的goroutine来做标记,但是一开始就gopark当前的goroutine(上个阶段),直到被gccontroller的findRunnableGCWorker唤醒。 唤醒后进入标记阶段,每个worker都去gc-work中拿节点(节点置黑),然后处理当前节点看有没有指针和没标记的对象,继续入队子节点(灰化节点),直到队列为空。 Gmarktermination 标记结束后调用gcMarkDone Gsweep 具体的清除行为,有多个时机可以出发Gsweep,如果是并发清除的话,需要先回收未被标记的heap区,然后唤醒进行sweep的 goroutine。 关于整体回收这一块儿内容,大家有兴趣可以看一下源码。 关于go的垃圾回收暂时就先介绍这么多。 关于go的内存管理后续会单独出一个系列,所以本系列仅仅阐述了一个内存管理的梗概和基础概念。