垃圾回收(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 加入混合写屏障
Go V1.3的标记清除
标记清除的流程
触发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的三色标记法
三色分别指的是:
白色标记表
灰色标记表
黑色标记表
三色标记的过程
1.开始标记,只要是新创建的对象,默认颜色都是标记为"白色",以上图为例,当前对象1-7都是白色
2.从程序(对象根节点)出发,即从上图的程序出发,开始遍历所有对象,把遍历到的对象从白色集合放入到灰色集合,当前对象1,对象4为灰,其余为白
3.遍历灰色集合,将灰色对象应用的对象从白色集合放入灰色集合,之后将原灰色集合放入黑色集合,当前1,4为黑,2,7为灰,3,5,6为白
4. 重复第三步,直到灰色表中无任何对象
5. 回收所有白色标记的对象,也就是回收垃圾
三色标记法的探讨
首先呢,三色标记法的出现肯定是为了优化GC的效率,最重要的一点就是缩短STW的时间
问题: 三色标记法也是扫描整个堆栈空间,和之前的标记清除有什么优势呢?
答: 可能的方法之后就是把标记的过程移出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开始,所有对象默认都是白色。优先将栈上的可达对象全部扫描并标记为黑色(之后不再向插入写屏障时进行第二次重复扫描,无需STW)
GC期间,任何在栈上创建的新对象和引用白色对象,均为黑色
堆上被删除的白色对象(解除引用关系)标记为灰色
堆上被添加的白色对象标记为灰色
注意混合写屏障是Gc的一种屏障机制,所以只是当程序执行GC的时候,才会触发这种机制。
满足:
变形的弱三色不变式。结合了插入,删除写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。
总结
在V1.3采用的是传统普通的标记清除法,全程需要STW,效率极低。此时提升GC效率的重要方向就是在标记阶段缩短STW,然而传统的标记清除不能保证没有STW的情况下不丢失数据。V1.5版本提出三色标记法,结合插入写屏障或者是删除写屏障,能大大缩短STW的同时,保证不丢失对象。V1.8 结合了插入/删除写屏障的优点,提出了混合写屏障,在保证不丢失数据的同时,几乎没有STW的存在
GC触发的条件:
定量触发:Go 语言运行时的默认配置会在堆内存达到上一次垃圾收集的 2 倍时,触发新一轮的垃圾收集,这个行为可以通过环境变量 GOGC 调整,在默认情况下它的值为 100,即增长 100% 的堆内存才会触发 GC。
定时触发:如果一定时间内没有触发,就会触发新的循环,该出发条件由 runtime.forcegcperiod 变量控制,默认为 2 分钟;
手动触发: 用户程序会通过 runtime.GC 函数在程序运行期间主动通知运行时执行,该方法在调用时会阻塞调用方直到当前垃圾收集循环完成,在垃圾收集期间也可能会通过 STW 暂停整个程序:
空间不足时触发: 当前线程的内存管理单元中不存在空闲空间时,创建32KB以下的对象可能触发垃圾收集,创建32KB以上的对象时,一定会尝试触发