垃圾回收(Garbage Collection,简称GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的内存对象,让出存储资源。

GO语言的GC过程中不需要程序员手动执行

1.Go V1.3之前的标记-清除(mark and sweep)算法

golang 1.3之前主要使用标记-清除算法,此算法主要有两个步骤:

标记(mark phase)

清除(sweep phase)

1.1标记清除算法的具体步骤

Golang三色标记与混合写屏障_对象引用

步骤1.stop the world

暂停程序业务逻辑,分出可达和不可达对象,然后做上标记

Golang三色标记与混合写屏障_不变式_02

步骤2.标记

找出可达对象做上标记

Golang三色标记与混合写屏障_golang_03

步骤3.清除

标记完成,开始清除未标记的对象,结果如下

Golang三色标记与混合写屏障_不变式_04

步骤4.停止暂停

让程序继续跑,然后就会隔段时间重复这个过程,直到进程结束。

1.2标记-清除算法的缺点

缺点1:STW,stop the world

让程序暂停,在STW 过程中,CPU不执行用户代码,全部用于垃圾回收,导致程序出现卡顿。

缺点2:标记要扫描整个heap

缺点3:清除数据会产生heap碎片

2.Go v1.3将STW的步骤迁移

go 1.3之前GC步骤如下

Golang三色标记与混合写屏障_golang_05

从上图看来,全部的GC时间都在STW范围中,这样会导致程序暂停的时间过长,影响程序性能。

Go 1.3b版本做了简单优化,将STW的步骤提前,减少STW暂停的时间范围。

Golang三色标记与混合写屏障_不变式_06

上图主要是将STW的步骤提前了一步,因为sweep清除的时候,可以不STW,因为这些对象已经是不可达的对象了,不会出现回收、写冲突的问题。

但是,仍然面临这个STW 的问题,会暂停整个程序

3.Go v1.5的三色并发标记法

Golang重的垃圾回收主要使用三色标记法,GC过程和用户程序可并发执行,但是需要一定时间的STW

3.1三色标记的步骤

步骤1:新建的对象,默认标记为“白色”

Golang三色标记与混合写屏障_对象引用_07

“程序“是一些对象的根结点的集合,我们将程序展开,会得到如下的表现形式

Golang三色标记与混合写屏障_不变式_08


步骤2:从根节点遍历对象

Golang三色标记与混合写屏障_golang_09

每次GC,都会从根节点遍历所有对象,把遍历到的对象放入"灰色"集合中。

如上图,当前可达对象为对象1和对象4,所以这两个对象被标记为灰色。


步骤3:灰色对象放入黑色集合,灰色对象引用的对象放入灰色集合

Golang三色标记与混合写屏障_golang_10

这一次只扫描灰色对象,将灰色对象的第一层可达对象由白色变为灰色,如对象2和对象7。

上一轮的灰色对象1和对象4则会被标记为黑色,同时从灰色标记表中移动到黑色标记表中。

步骤4:重复第三步,直到灰色中没有任何对象

Golang三色标记与混合写屏障_对象引用_11

Golang三色标记与混合写屏障_不变式_12

当全部遍历完成,只有黑色和白色对象。黑色保留,白色需要被清除。

步骤5:回收所有的白色对象,即垃圾回收

Golang三色标记与混合写屏障_对象引用_13

上面我们说过,三色标记GC过程和用户程序可并发执行,执行并发过程内存可能相互依赖,为了保证GC过程数据的安全,需要在开始三色标记之前加上STW,在扫描确定黑白对象之后再放开STW。

那么Go是如何处理STW问题的呢?我们先做如下假设

3.2没有STW的三色标记

我们把初始状态设置为已经经历了一轮扫描,目前黑色的有对象1和对象4,灰色的有对象2和对象7,其他的是白色对象,且对象2通过指针指向对象3,如图所示

Golang三色标记与混合写屏障_golang_14

现在如果没有STW,在GC扫描的过程中,任意对象可能发生读写操作。

如下图:在没有扫描到对象2的时候,标记为黑色的对象4,此时创建指针q指向白色的对象3。

Golang三色标记与混合写屏障_golang_15

与此同时对象2将指针p移除,那么白色的对象3目前就被挂在了扫描完成的黑色的对象4下。

Golang三色标记与混合写屏障_golang_16

然后继续三色标记算法,将所有灰色对象标记为黑色,对象2和对象7就被标记成了黑色。

Golang三色标记与混合写屏障_golang_17

现在执行三色标记最后一步,清理白色对象。

Golang三色标记与混合写屏障_golang_18

从上面例子中可以看出,有两种情况在三色标记算法中是不希望发生的

条件一:一个白色对象被黑色对象饮用(白色挂在黑色下)

条件二:灰色对象和它关联的白色对象失去关联(灰色同时丢失了白色)

当上面两个条件同时满足时,就会出现对象丢失的现象。

为了防止上面这种现象,最简单的解决方式就是STW,但是STW有明显的资源浪费,对用户程序有很大的影响,那有什么机制能确保对象不丢失还能提高GC效率呢?

那就是有一种机制,尝试去破坏上面两个必要条件。

3.3平屏障机制--避免对象丢失

我们让GC回收器,满足下面两种情况之一,即可保证对象不丢失。

这两种模式就是“弱三色不变性和强三色不变性”。

3.3.1强-弱三色不变性

强三色不变性--不存在黑色对象引用到白色对象的指针

不允许黑色对象饮用白色对象,这样就不会有白色对象杯误删除。

Golang三色标记与混合写屏障_对象引用_19


弱三色不变性--所有被黑色引用的白色对象,都同时有一个灰色对象引用用该白色对象

Golang三色标记与混合写屏障_对象引用_20

若三色不变式强调,黑色对象可以引用白色对象,但白色对象必须也同时被其他灰色对象引用或可达该白色对象的链路上游有灰色对象。

这样白色对象就被灰色对象保护起来了,不会被删除。

为了遵循上面两个方式,GC算法引入了两种屏障方式,分别为“插入屏障”和“删除屏障”

3.3.2插入屏障--黑色对象A新增引用B,将B标记为灰色

具体操作:

在黑色A对象新增引用B对象的时候,B对象标记为灰色。(将B挂在A下游,B必须标记为灰色)

满足:

强三色不变式(不存在黑色对象应用白色对象的情况,因为白色对象强制变为了灰色)

过程演示:

堆内存启用插入屏障,栈内存没有启用。

Golang三色标记与混合写屏障_对象引用_21


Golang三色标记与混合写屏障_golang_22


Golang三色标记与混合写屏障_对象引用_23


Golang三色标记与混合写屏障_不变式_24


Golang三色标记与混合写屏障_不变式_25


Golang三色标记与混合写屏障_不变式_26

由于栈内存没有启用写入屏障,所以当全部对象扫描完成后,要对栈重新进行三色扫描,但这次为了对象不丢失,要对本次扫描启动STW暂停,直到栈空间的三色标记结束。

Golang三色标记与混合写屏障_不变式_27


Golang三色标记与混合写屏障_不变式_28


Golang三色标记与混合写屏障_不变式_29


最后将栈和堆空间全部的白色对象清除。这次STW大约的时间在10-100ms。

Golang三色标记与混合写屏障_golang_30


3.3.3删除屏障--被删除的对象,如果自身为白色或灰色,那么就把被删除的标记为灰色

具体操作:

被删除的对象,如果自身为白色或灰色,那么将被标记为灰色。

满足:

弱三色不变性(保护灰色对象到白色对象的路径不会断)

过程演示

Golang三色标记与混合写屏障_对象引用_31


Golang三色标记与混合写屏障_对象引用_32


Golang三色标记与混合写屏障_对象引用_33


Golang三色标记与混合写屏障_golang_34


Golang三色标记与混合写屏障_不变式_35


Golang三色标记与混合写屏障_不变式_36


Golang三色标记与混合写屏障_golang_37

删除屏障的回收精度低,一个对象即使被删除了最后一个指向它的指针,实际是可清除对象,但由于被标记为了灰色,需要下一轮GC才会被清理掉。


4.Go v1.8的混合屏障机制

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

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

删除写屏障:回收精度低。

4.1混合屏障的具体操作

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

2.GC期间,任何在栈上创建的对象,均为黑色。

3.被删的对象标记为灰色。

4.被添加的对象标记为灰色。

满足弱三色不变式。

4.2混合屏障场景介绍

扫描栈对象,将可达对象全部标记为黑

Golang三色标记与混合写屏障_golang_38

Golang三色标记与混合写屏障_不变式_39

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

Golang三色标记与混合写屏障_golang_40


Golang三色标记与混合写屏障_不变式_41


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

Golang三色标记与混合写屏障_golang_42


Golang三色标记与混合写屏障_golang_43


Golang三色标记与混合写屏障_不变式_44

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


Golang三色标记与混合写屏障_对象引用_45


Golang三色标记与混合写屏障_不变式_46


Golang三色标记与混合写屏障_golang_47


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

Golang三色标记与混合写屏障_不变式_48


Golang三色标记与混合写屏障_不变式_49


Golang三色标记与混合写屏障_对象引用_50


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

5.总结

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

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

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

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

参考文献