Go GC
Go GC
“GC Roots” 的对象选择
JAVA的GC Root对象选择
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 本地方法栈(Native 方法)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- Java虚拟机内部引用;
- 所有被同步锁持有的对象
Go的GC Root对象
即全局变量和G Stack中的引用指针,简单来说就是全局量和go程中的引用指针
图中allocCount应为4,
gcmarkBits用于在gc时标记哪些对象存活, 每次gc以后gcmarkBits会变为allocBits.
STW
stop the world是gc的最大性能问题,对于gc而言,需要停止所有的内存变化,即停止所有的goroutine,等待gc结束之后才恢复
GC触发
- 阈值:默认内存扩大一倍,启动gc
- 定期:默认2min触发一次gc,src/runtime/proc.go:forcegcperiod
- 手动:runtime.gc()
Go V1.3版本之前
Go V1.3版本之前就是以上来实施的, 在执行GC的基本流程就是首先启动STW暂停,然后执行标记,再执行数据回收,最后停止STW,如图所示。
Go V1.3 做了简单的优化,将STW的步骤提前, 减少STW暂停的时间范围.如下所示
上图主要是将STW的步骤提前了异步,因为在Sweep清除的时候,可以不需要STW停止,因为这些对象已经是不可达对象了,不会出现回收写冲突等问题。
但是无论怎么优化,Go V1.3都面临这个一个重要问题,就是mark-and-sweep 算法会暂停整个程序 。
Go V1.5三色标记
- 灰色:对象已被标记,但这个对象包含的子对象未标记
- 黑色:对象已被标记,且这个对象包含的子对象也已标记,gcmarkBits对应的位为1(该对象不会在本次GC中被清理)
- 白色:对象未被标记,gcmarkBits对应的位为0(该对象将会在本次GC中被清理)
当前内存中有A~F一共6个对象,根对象a,b本身为栈上分配的局部变量,根对象a、b分别引用了对象A、B, 而B对象又引用了对象D,则GC开始前各对象的状态如下图所示:
- 初始状态下所有对象都是白色的。
- 接着开始扫描根对象a、b; 由于根对象引用了对象A、B,那么A、B变为灰色对象,接下来就开始分析灰色对象,分析A时,A没有引用其他对象很快就转入黑色,B引用了D,则B转入黑色的同时还需要将D转为灰色,进行接下来的分析。
- 灰色对象只有D,由于D没有引用其他对象,所以D转入黑色。标记过程结束
- 最终,黑色的对象会被保留下来,白色对象会被回收掉。
如上的三色标记,如果不启动STW,并发的垃圾回收,仍然会出现问题;
会造成对象丢失的情形:
条件1: 一个白色对象被黑色对象引用(黑色已遍历过,不会再次遍历)
条件2: 该白色对象没有被灰色引用,或灰色引用被破坏(灰色同时丢了该白色对象)
对象3会被回收
GC流程
- Stack scan:Collect pointers from globals and goroutine stacks。收集根对象(全局变量,和G stack),开启写屏障。全局变量、开启写屏障需要STW,G stack只需要停止该G就好,时间比较少。
- Mark: Mark objects and follow pointers。标记所有根对象, 和根对象可以到达的所有对象不被回收。
- Mark Termination: Rescan globals/changed stack, finish mark。重新扫描全局变量,和上一轮改变的stack(写屏障),完成标记工作。这个过程需要STW。
- Sweep: 按标记结果清扫span
目前整个GC流程会进行两次STW(Stop The World), 第一次是Stack scan阶段, 第二次是Mark Termination阶段.
- 第一次STW会准备根对象的扫描, 启动写屏障(Write Barrier)和辅助GC(mutator assist).
- 第二次STW会重新扫描部分根对象, 禁用写屏障(Write Barrier)和辅助GC(mutator assist).
从1.8以后的golang将第一步的stop the world 也取消了,这又是一次优化; 1.9开始, 写屏障的实现使用了Hybrid Write Barrier, 大幅减少了第二次STW的时间.
屏障机制
(1) “强-弱” 三色不变式
强三色不变式:不存在黑色对象引用到白色对象的指针(破坏条件一)。
弱三色不变式:所有被黑色对象引用的白色对象都处于灰色保护状态(破坏条件二)。
(2) 插入写屏障
对象被引用时,触发的屏障(屏障,相当于拦截器,额外操作);
具体操作: 在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色);
满足方式: 强三色不变式
栈空间的特点是容量小,但是要求响应速度快,因为函数调用弹出频繁使用;
所以“插入写屏障”机制,在栈空间的对象操作中不使用(启动STW,重新标记),而仅仅使用在堆空间使用;
(3) 删除写屏障
具体操作: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
满足: 弱三色不变式. (保护灰色对象到白色对象的路径不会断)
删除1到5的引用,那么5会被标记为灰色。
删除对象6即可
(4) 混合写屏障机制
Go V1.8之后混合写屏障机制
插入写屏障和删除写屏障的缺点:
插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;
删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象;
1.混合写屏障规则
具体操作:
1)GC开始将栈上的对象全部引用扫描并标记为黑色 (之后不再进行第二次重复扫描,无需STW)。
2)GC期间,任何在栈上创建的新对象,均为黑色(保证栈全为黑色对象)。
3)被删除的对象标记为灰色(删除写屏障)。
4)被添加的对象标记为灰色(插入写屏障)。
满足: 变形的弱三色不变式
2. 混合写屏障的具体场景
1)GC开始,全部对象都为白色
2)扫描栈对象,全部标记为黑色
场景一: 对象被一个堆对象删除引用,成为栈对象的下游
场景二: 对象被一个栈对象删除引用,成为另一个栈对象的下游
![image-20211022145823307](/Users/zhulingang/Library/Application Support/typora-user-images/image-20211022145823307.png)
场景三:对象被一个堆对象删除引用,成为另一个堆对象的下游
场景四:对象从一个栈对象删除引用,成为另一个堆对象的下游
Golang中的混合写屏障满足弱三色不变式
,结合了删除写屏障和插入写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。
总结
以上便是Golang的GC全部的标记-清除逻辑及场景演示全过程。
GoV1.3- 普通标记清除法,整体过程需要启动STW,效率极低。
GoV1.5- 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通
GoV1.8-三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW,效率较高。