gc 三色标记(三色标记法)

佚名 举报 | 买帖

本文小编围绕 gc 三色标记(三色标记法)做一个相关介绍。本文共计3640个字,预计阅读时长13分钟。

gc 三色标记,原文标题:三色标记法的GC可以高效率地处理垃圾。

垃圾回收(Garbage Collection,简称GC)是go语言中提供的内存管理机制,自动释放不需要的内存对象。GC过程无需程序员手动执行。

Go在GC的演进过程中经历很多次变革:

版本

GC算法

v1.1

STW(Stop the world)

v1.3

Mark STW,Sweep(标记清除)

v1.5

三色标记

v1.8

三色标记 写屏障

一、标记-清除算法

此算法主要有两个步骤

LifE54。ZHilI123.COM

  • 标记
  • 清除

1.1、具体步骤

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

图示表示程序与对象的可达关系,目前程序的可达对象有1-2-3,4-7共5个对象

2、开始标记,程序找出它所有可达的对象,并做上标记

3、标记完之后,开始清除未标记的对象

mark and sweep算法在执行时,需要程序暂停,即STW,STW过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程影响很大。

4、STW结束,程序继续运行,直到程序生命周期结束。

1.2、缺点

  • STW,让程序暂停,程序出现卡顿
  • 标记需要扫描整个heap
  • 清除数据会产生heap碎片

Go1.3版本之前按照启动STW->Mark标记->Sweep清除->停止STW这个流程,全部的GC时间都在STW范围内,Go1.3版本做了简单优化,因为Sweep清除可以不需要STW停止,因为这些对象都是不可达对象,不会出现回收写冲突的问题。

二、三色标记法

Go中的垃圾回收主要应用三色标记法,GC过程和其他goroutine可并发运行,但需要一定时间STW。

2.1、具体步骤

三色标记法其实就是通过三个阶段的标记对象的状态。

1、每次新创建的对象,默认都被标记为“白色”

我们将程序展开

2、每次GC回收开始,会从根结点开始遍历所有对象,把遍历到的对象从白色集合放入到灰色集合中

3、遍历灰色集合,将灰色引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合

4、重复第三步,直到灰色中无任何对象

当我们全部的可达对象遍历完后,灰色标记中不再存在灰色对象,全部内存的数据只有黑色和白色。那么黑色对象就是程序逻辑可达(需要)的对象,是合法有用的数据,支持程序运行的数据,不可删除,而白色对象就是全部不可达数据,就是内存中的垃圾数据,需要被清除。

LifE54。ZHilI123.COM

LifE54。ZHilI123.COM

5、回收所有的白色对象,也就是回收垃圾

可以看出这里面可能会有很多并发流程都会被扫描,执行并发流程的内存可能会互相依赖,为了在GC过程中保护数据安全,在三色标记前加STW,在扫描确定黑白对象后放开STW。但是很明显这样的GC扫描性能太低了。

如果不添加STW,在标记完黑色对象后,有可能黑色对象重新指向新的对象,而这个新的对象是不能被扫描到的。

三、屏障机制

LifE54。ZHilI123.COM

让GC满足以下两种情况时,保证对象不丢失。

3.1、强-弱三色不等式

1、强三色不等式

不存在黑色对象引用到白色对象的指针。也就是说,强制性的不允许黑色对象引用白色对象,只能引用灰色对象,这样就不会出现白色对象被误删的情况。

2、弱三色不等式

所有被黑色对象引用的白色对象都处于灰色保护状态。

黑色对象可以引用白色对象,白色对象存在其他灰色对象对它的引用,或可达它的链路上游存在灰色对象。这样实则是黑色对象引用白色对象,白色对象处于一个被删除的状态,但是上游灰色对象的引用,可以保护白色对象,使其安全。

为了遵循上述两种方式,GC算法演进到两种屏障方式,“插入屏障”和“删除屏障”

3.2、插入屏障

在A对象引用B对象时,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色)

满足强三色不等式。

插入屏障机制在栈空间的对象操作不使用,仅仅使用在堆空间对象的操作中。

1、程序创建时,全部对象标记为白色

2、遍历Root Set,得到灰色对象

3、遍历灰色对象,将可达对象,从白色标记为灰色,灰色标记为黑色

4、由于并发特性,此时向对象1添加对象8,对象4添加对象9,触发插入屏障机制,对象1不触发

5、由于插入写屏障,对象9变成灰色对象,对象8依然为白色对象

6、继续上述流程进行三色标记,知道没有灰色节点

7、在准备回收白色对象前,重新扫描遍历一次栈空间,此时加STW暂停保护栈,防止外界干扰(有新的白色被黑色添加)

8、在STW中,将栈中的对象一次三色标记,知道没有灰色节点

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

3.3、删除屏障

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

满足弱三色不等式

1、程序初创建,所有对象被标记为白色

2、遍历Root Set,得到灰色节点

3、灰色对象1删除对象5,对象5被标记为灰色

4、继续遍历灰色对象,将可达对象从白色标记为灰色,遍历后的灰色标记为黑色

5、继续循环上述流程,知道没有灰色节点

6、清除白色节点

这种回收方式精度低,一个对象即时被删除了也可以活过这一轮GC,在下一轮GC被清除掉

四、混合写屏障

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

  • 插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象存活
  • 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录快照,这个过程会保护开始时刻的所有的存活对象

Go1.8引入混合写屏障机制,避免了对栈的重复扫描过程,极大减少了STW的时间。

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

2、GC期间,任何在栈上创建的新对象,都标记为黑色

3、被删除的对象标记为灰色

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

具体场景

1、GC开始,默认为白色

2、优先扫描栈对象,将可达对象标记为黑色


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

1、将对象7添加到对象1下游,因为栈不启用写屏障,所以直接挂在下面

2、对象4删除对象7的引用关系,因为对象4是堆区,所以触发写屏障,标记被删除的对象7为灰

LifE54。ZHilI123.COM

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

1、新建一个对象9在栈上,混合写屏障中,GC过程中任何新创建的对象均标记为黑色

2、对象9添加下游引用栈对象3,直接添加,栈不启用屏障,无屏障效果

3、对象2删除对象3的引用关系,是直接删除,栈不启用屏障,无屏障效果

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

1、新建一个堆对象10,已经被扫描标记为黑色

2、堆对象10添加下游引用堆对象7,触发屏障机制,被添加的对象标记为灰色

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

1、栈对象1删除栈对象2的引用

2、堆对象4删除对象7的引用关系,添加引用到对象2

3、对象4在删除的时候触发写屏障,标记被删除的对象7为灰色


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

五、垃圾回收过程

5.1、Marking setup

为了打开写屏障,必须停止每个goroutine,让垃圾收集器观察并等待每个goroutine进行函数调用,等待函数调用是为了保证goroutine停止时处于安全点。

下面的代码中,由于for{} 循环所在的goroutine 永远不会中断,导致始终无法进入STW阶段,资源浪费;Go 1.14 之后,此类goroutine 能被异步抢占,使得进入STW的时间不会超过抢占信号触发的周期,程序也不会因为仅仅等待一个goroutine的停止而停顿在进入STW之前的操作上。

func main() { go func() { for { } }() time.Sleep(time.Milliecond) runtime.GC() println("done")}

5.2、Marking

一旦写屏障打开,垃圾收集器就开始标记阶段。

标记阶段需要标记在堆内存中仍然在使用中的值。首先检查所有现goroutine的堆栈,以找到堆内存的根指针。然后收集器必须从那些根指针遍历堆内存图,标记可以回收的内存。

当存在新的内存分配时,会暂停分配内存过快的那些 goroutine,并将其转去执行一些辅助标记(Mark Assist)的工作,从而达到放缓继续分配、辅助 GC 的标记工作的目的。

5.3、Mark终止

关闭写屏障,执行各种清理任务(STW)

5.4、Sweep清理

清理阶段用于回收标记阶段中标记出来的可回收内存。当应用程序goroutine尝试在堆内存中分配新内存时,会触发该操作,清理导致的延迟和吞吐量降低被分散到每次内存分配时。

六、如何触发GC

1、runtime.GC()强制触发GC

2、再分配内存时,判断当前内存是否达到阈值会触发新一轮GC(比如当前为4MB,GOGC=100,4MB 4MB*GOGC/100)

3、上次GC间隔达到了runtime.forcegcperiod(默认2分钟),会启动GC

调优方法

1、合理化内存分配速度

2、降低并复用已经申请的内存

3、调整GOGC