Go GC

Go GC

Go GC

“GC Roots” 的对象选择

JAVA的GC Root对象选择

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 本地方法栈(Native 方法)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • Java虚拟机内部引用;
  • 所有被同步锁持有的对象

Go的GC Root对象

即全局变量和G Stack中的引用指针,简单来说就是全局量和go程中的引用指针

image-20211022002907496

图中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暂停的时间范围.如下所示

image-20211022151619728

上图主要是将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开始前各对象的状态如下图所示:

  1. 初始状态下所有对象都是白色的。
  2. 接着开始扫描根对象a、b; 由于根对象引用了对象A、B,那么A、B变为灰色对象,接下来就开始分析灰色对象,分析A时,A没有引用其他对象很快就转入黑色,B引用了D,则B转入黑色的同时还需要将D转为灰色,进行接下来的分析。
  3. 灰色对象只有D,由于D没有引用其他对象,所以D转入黑色。标记过程结束
  4. 最终,黑色的对象会被保留下来,白色对象会被回收掉。

image-20211022003235402

如上的三色标记,如果不启动STW,并发的垃圾回收,仍然会出现问题;

会造成对象丢失的情形:

条件1: 一个白色对象被黑色对象引用(黑色已遍历过,不会再次遍历)

条件2: 该白色对象没有被灰色引用,或灰色引用被破坏(灰色同时丢了该白色对象)

image-20211022005351474

对象3会被回收

GC流程

  1. Stack scan:Collect pointers from globals and goroutine stacks。收集根对象(全局变量,和G stack),开启写屏障。全局变量、开启写屏障需要STW,G stack只需要停止该G就好,时间比较少。
  2. Mark: Mark objects and follow pointers。标记所有根对象, 和根对象可以到达的所有对象不被回收。
  3. Mark Termination: Rescan globals/changed stack, finish mark。重新扫描全局变量,和上一轮改变的stack(写屏障),完成标记工作。这个过程需要STW。
  4. 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) “强-弱” 三色不变式

强三色不变式:不存在黑色对象引用到白色对象的指针(破坏条件一)。

image-20211022005954004

弱三色不变式:所有被黑色对象引用的白色对象都处于灰色保护状态(破坏条件二)。

image-20211022010012682

(2) 插入写屏障

对象被引用时,触发的屏障(屏障,相当于拦截器,额外操作);

具体操作: 在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色);

满足方式: 强三色不变式

栈空间的特点是容量小,但是要求响应速度快,因为函数调用弹出频繁使用;

所以“插入写屏障”机制,在栈空间的对象操作中不使用(启动STW,重新标记),而仅仅使用在堆空间使用;

image-20211022142911365

image-20211022142939484

image-20211022143032543

image-20211022143056009

image-20211022143139543

(3) 删除写屏障

具体操作: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。

满足: 弱三色不变式. (保护灰色对象到白色对象的路径不会断)

image-20211022143641243

删除1到5的引用,那么5会被标记为灰色。

image-20211022144755363

删除对象6即可

(4) 混合写屏障机制

Go V1.8之后混合写屏障机制

插入写屏障和删除写屏障的缺点:

插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;

删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象;

1.混合写屏障规则

具体操作:

1)GC开始将栈上的对象全部引用扫描并标记为黑色 (之后不再进行第二次重复扫描,无需STW)。

2)GC期间,任何在栈上创建的新对象,均为黑色(保证栈全为黑色对象)。

3)被删除的对象标记为灰色(删除写屏障)。

4)被添加的对象标记为灰色(插入写屏障)。

满足: 变形的弱三色不变式

2. 混合写屏障的具体场景

1)GC开始,全部对象都为白色

image-20211022144317315

2)扫描栈对象,全部标记为黑色

image-20211022144342286

场景一: 对象被一个堆对象删除引用,成为栈对象的下游

image-20211022145210788

image-20211022145235754

场景二: 对象被一个栈对象删除引用,成为另一个栈对象的下游

image-20211022145752970

![image-20211022145823307](/Users/zhulingang/Library/Application Support/typora-user-images/image-20211022145823307.png)

image-20211022145836721

场景三:对象被一个堆对象删除引用,成为另一个堆对象的下游

image-20211022150206835

image-20211022150217882

image-20211022150243212

场景四:对象从一个栈对象删除引用,成为另一个堆对象的下游

image-20211022150436773

image-20211022150551349

image-20211022150605069

Golang中的混合写屏障满足弱三色不变式,结合了删除写屏障和插入写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。

总结

以上便是Golang的GC全部的标记-清除逻辑及场景演示全过程。

GoV1.3- 普通标记清除法,整体过程需要启动STW,效率极低。

GoV1.5- 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通

GoV1.8-三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW,效率较高。


   转载规则


《Go GC》 朱林刚 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Go Defer Go Defer
DeferDeferdefer的执行顺序多个defer出现的时候,它是一个“栈”的关系,也就是先进后出。一个函数中,写在前面的defer会比写在后面的defer调用的晚。 func main() { defer func1()
2021-11-18
下一篇 
Go Contex Go Contex
Go ContexContexcontext包能够提供一个请求从API请求边界到各goroutine的请求域数据传递、取消信号及截至时间等能力。详细原理请看下文。 在 Go 语言中 context 包容许您传递一个 “context” 到您
2021-11-18
  目录