垃圾回收(Garbage Collection,简称GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的对象,让出存储器资源,无需程序员手动执行。
GOLANG的垃圾回收机制现在go1.14所用的是三色标志法和GC混合写屏障,GC过程和其他用户goroutine可并发运行,但需要一定时间的STW(stop the world),STW的过程中,CPU不执行用户代码,全部用于垃圾回收
GC版本变化Go V1.3 标记清除(mark and sweep)
Go V1.5 三色标记法
Go V1.8 加入混合写屏障
标记清除的流程
- 触发GC,暂停程序业务逻辑(STW),找出可达对象和不可达对象
如图所示,箭头表示引用关系,1,2,3,4,7这五个对象就是可达对象,5和6这两个对象就是不可达对象 - 开始标记,找到它所有的可达对象,并做上标记,如上图中的红色对象部分。
- 完成标记,开始清除未标记的对象
- 完成清除,停止暂停,然后循环重复以上四步,直到该程序进程生命周期结束
整个流程的图示:
问题:触发GC的条件是什么
标记清除的缺点
- 存在STW。因为在GC阶段需要暂停整个程序,程序出现卡顿,严重影响性能(最重要的 )
- 标记过程需要扫描整个heap和stack(堆栈信息),无疑是加长了整个STW的持续时间,还是影响性能的问题
- 清除数据会产生heap碎片,也就是产生一些不连续的碎片内存空间,对后续的重用,重组增加了困难
优化标记清除
标记清除的GC策略最主要问题在于STW时间过长,优化方向就是减少STW时间,如下图:
即调整GC流程的执行顺序,将Sweep操作挪出STW,使之在和程序业务逻辑并发执行,以此达到缩短STW时间的目的
但是效果有限,前面说到的找到可达对象可标记的过程涉及整个内存空间,占了时间大头。那么优化GC要从标记出发,想方设法地在标记阶段减少STW,但传统普通的标记清除难以保证在没有STW的情况下不丢失对象(该部分后面有反复提及),所以之后摈弃了传统的标记方法,提出了三色标记法
Go V1.5的三色标记法三色分别指的是:
- 白色标记表
- 灰色标记表
- 黑色标记表
三色标记的过程
从白色集合放入到灰色集合
三色标记法的探讨
首先呢,三色标记法的出现肯定是为了优化GC的效率,最重要的一点就是缩短STW的时间
问题: 三色标记法也是扫描整个堆栈空间,和之前的标记清除有什么优势呢?
会误删除引用对象
- 一个白色对象当前被黑色对象引用(即白色被挂在黑色下)
- 灰色对象与该白色之间的引用关系被破坏(即灰色同时丢失了该白色)
看了上面的问答之后,要想在标记阶段没有STW的存在,就要阻止以上两个条件同时发生的情况
强三色不变式弱三色不变式
强三色不变式
强制性的不允许黑色对象引用白色对象,破坏条件1.
弱三色不变式
黑色对象可以引用白色,但同时该白色对象必须存在其他灰色对象对它的引用,或者是在该白色对象引用链路的上游存在灰色对象,破坏条件2
在三色标记中只要满足强/弱三色不变式的一种,即可保证引用对象不丢失
总结:
三色标记法相比传统标记,对GC的效率提升就是要将标记的过程挪出STW。但三色标记在如两个条件同时满足时会丢失引用对象,后来提出强/弱三色不变式规则来防止两个条件同时成立,下面的问题就是如何实现强/弱三色不变式规则,即屏障机制
屏障机制
实现强/弱三色不变式
可以分为zhen
- 插入写屏障(对象被引用时出发的机制)
- 删除写屏障(对象被删除时,也即解除引用关系时触发的机制)
插入写屏障
具体操作:在A对象引用B对象的时候,将B对象标记为灰色(如果要将B挂在A的下游,B必须要被标记为灰色)
满足:强三色不变式(不会存在黑色对象引用白色对象的情况,因为白色对象会被强制变为灰色对象)
伪码:
插入屏障只限制堆上的对象,不限制栈上的对象
我们知道对象存储是在堆上或者是栈上,因为每次插入都要做判断的话会影响性能。如果运行时需要在几百个 Goroutine 的栈上都开启写屏障,会带来巨大的额外开销,又加上栈空间比较小,但是要求相应速度快,因为函数调用弹出频繁使用,所以在栈上是没有插入屏障的,只在堆上有
插入写屏障的不足
因为插入屏障不限制栈上对象的原因,所以在三色标记法标记完整个heap之后,要启动STW,把栈上黑色对象全部置为白色,重新遍历扫描一次栈空间。因为栈空间比较小,所以耗时很短,大约需要10-100ms
删除写屏障
具体操作:被删除(解除引用关系)的对象,如果自身为白色,那么被标记会灰色
满足:弱三色不变式(保护灰色对象到白色对象的路径不会断)
删除屏障的不足:
回收精度低,一个对象即使被删除了最后一个指向它的指针,它也依旧可以活过这一轮GC,在下一轮GC中才能被清除掉。不过该问题只是影响当前可用内存大小,可以忽略
总结
1.5版本在标记过程中使用三色标记法。回收过程主要有四个阶段,其中,标记和清理都并发执行的,但标记阶段的前后需要STW一定时间来做GC的准备工作和栈的re-scan
Go V1.8的三色标记法+混合写屏障机制前面介绍了三色标记法的过程。同时直到要想提高GC的效率,需要把标记阶段没有STW,但在两个条件同时满足时,会丢失对象。为了避免两个条件同时发生,提出了强/弱三色不变式,对应的实现分别是插入写屏障和删除写屏障。但两个都有自己的不足,混合写屏障为了保证性能,不能在栈上对象执行,仍然需要STW来保障标记。删除写屏障回收精度低(同时在回收前仍需要短暂STW来记录当前快照)
针对以上不足,Go在V1.8结合了插入写屏障和删除写屏障的优点,提出混合写屏障(hybrid write barrier)机制
具体操作:
栈上的可达对象栈上创建的新对象
注意混合写屏障是Gc的一种屏障机制,所以只是当程序执行GC的时候,才会触发这种机制。
满足:
变形的弱三色不变式。结合了插入,删除写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。
传统的标记清除不能保证没有STW的情况下不丢失数据大大缩短STW的同时,保证不丢失对象。在保证不丢失数据的同时,几乎没有STW的存在
GC触发的条件:
- 定量触发:Go 语言运行时的默认配置会在堆内存达到上一次垃圾收集的 2 倍时,触发新一轮的垃圾收集,这个行为可以通过环境变量 GOGC 调整,在默认情况下它的值为 100,即增长 100% 的堆内存才会触发 GC。
- 定时触发:如果一定时间内没有触发,就会触发新的循环,该出发条件由 runtime.forcegcperiod 变量控制,默认为 2 分钟;
- 手动触发: 用户程序会通过 runtime.GC 函数在程序运行期间主动通知运行时执行,该方法在调用时会阻塞调用方直到当前垃圾收集循环完成,在垃圾收集期间也可能会通过 STW 暂停整个程序:
- 空间不足时触发: 当前线程的内存管理单元中不存在空闲空间时,创建32KB以下的对象可能触发垃圾收集,创建32KB以上的对象时,一定会尝试触发
参考文章:
https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/