前言

Go语言和其他多数高级编程语言如Java一样,编程语言提供了自动进行内存管理的机制,称之为Garbage Collection(GC);GC自动释放不再使用的对象所占有的内存,而不需要开发人员手动销毁去销毁此对象的内存空间;其中过程中完全有GC机制处理,而无需开发人员手工介入。

深入浅出GO GC垃圾回收_开发人员

虽然GC机制是一种自动机制,无需开发人员处理,但是认识到GC的内部机制和运行原理,可以让我们开发人员在GC认知的基础上更高效的去进行GO语言程序的编码,从而能够写出更高效的,更稳定性,更优雅的程序,毕竟GO语言是处理底层服务的利器,而高性能,稳定性就是底层服务是否优秀的评分标准。

GC的方式

其实有很多介绍GC的文章,各种不同的编程语言都有;但是大多数的文章都把GC描述的过于神秘,不可否认GC在实现的过程中确实非常的复杂(细节上);所以描述起来也不容易理解,本文就是想从浅入深的去认知GC;让大家理解GC更加的轻松容易。

根据我们所描述GC所处理的内容,简单的可以看到,其实GC要做的就是, 1. 识别无用的垃圾对象, 2。 找到没有使用的对象,并进行销毁。 非常简单的动机,但是考虑到GC过程对程序运行的影响;所以会有各种不同的策略来实现这两部,道相同,术不同,而这个术,就是对GC的优秀的考量;

标记清除算法(Mark Sweep)

标记清除算法是最常见的垃圾收集算法,标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段:

  1. 标记阶段:暂停应用程序的执行,从根对象触发查找并标记堆中所有存活的对象; 这个过程我们也称之为STW(Stop The World)
  2. 清除阶段:遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表,恢复应用程序的执行;

深入浅出GO GC垃圾回收_jvm_02

计数器法(Counting)

使用计算器记录对象的引用情况,被引用加1,取消引用减1;理解起来非常简单,实现也比较简单,由于直接就在对象上直接获取情况,不需要递归循环,效率相对来说也高; 但是,有计数器的方式,除了需要多余空间来保持计数, 还无法处理循环的引用;标识上却是高效,但是会频繁的去更新计数,反而性能低下(虽然每一个动作快,但是频繁做,累计起来就不快了)

追踪可达法(tracing)

通过分析引用对象的关系,对一个对象是否可达进行分析;如果对象一旦不可达就可以立刻被GC回收了

深入浅出GO GC垃圾回收_算法_03

可以解决Counter存在的不支持循环引用的问题,而且不需要多余计数的内存空间的优点。

标记清除方式实现相对来说容易实现;但是缺点也很明显

  1. 执行期间需要STW;把整个程序完全暂停,不能异步的进行垃圾回收。
  2. 由于是清理的不可达对象的内存空间;容易产生大量不连续的内存随便,碎片太多可能会导致后续没有足够的连续内存分配给较大的对象,从而提前触发新的一次垃圾收集动作。

三色并发标记法

为了解决原始标记清除算法带来的长时间STW, Go从v1.5版本实现了基于三色标记清除的并发垃圾收集器,

在Tracing的方式基础上,在可达分析的基础上;把遍历对象图过程中遇到的对象,按 “是否访问过” 这个条件标记成以下三种颜色:

白色:尚未被GC访问过的对象,如果全部标记已完成依旧为白色的,称为不可达对象,既垃圾对象。

黑色:本对象已经被GC访问过,且本对象的子引用对象也已经被访问过了。

灰色:本对象已访问过,但是本对象的子引用对象还没有被访问过,全部访问完会变成黑色,属于中间态。

深入浅出GO GC垃圾回收_算法_04

动图解析

步骤一:在GC并发标记刚开始时,所以对象均为白色集合;

步骤二:将所有GCRoots直接引用的对象标记为灰色集合;

步骤三:判断灰色集合中的对象:

若对象 不存在 子引用,则将其放入 黑色集合 ;

若对象 存在 子引用对象,则 将其所有的子引用对象放入灰色集合,当前对象放入黑色集合。

步骤四:按照步骤三,以此类推,直至灰色集合中的所有对象变成黑色后,本轮标记完成,且当前白色集合内的对象称为不可达对象,既垃圾对象。

三色并发标记法是在和用户线程并发运行的情况下,对象的引用处于随时可变的情况下,那么就会造成多标和漏标的问题。如上动图里所里的,本应该被标记为白色的对象,由于此时业务程序的并发运行,导致没有被标记,造成该对象可能不会被回收——我们称之为浮动垃圾;可能会出现野指针(指向没有合法地址的指针),从而造成严重的程序错误;或者由于并发的影响;灰色对象指向白色对象的引用消失了,然后一个黑色的对象重新引用了白色对象。这种错误必浮动垃圾更为严重——我们称之为漏标;错误的回收非垃圾对象;导致访问可能出现的无效地址;

三色不变性

想要在并发或者增量的标记算法中保证正确性,我们需要达成一下两种三色不变性中的任意一种

  • 强三色不变性——黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象。
  • 弱三色不变性——黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径。

或者通过屏蔽技术,达到以上两种不变性

屏障技术

垃圾收集中的屏障技术更像是一个钩子方法,它是在用户程序读取对象、创建新对象以及更新对象指针时执行的一段代码,根据操作类型的不同,我们可以将它们分成读屏障和写屏障两种,因为读屏障需要在读操作中加入代码片段,对用户程序的性能影响很大,所以变成语言往往都会采用写屏障保证三色不变性。

  • 插入写屏障——当一个对象引用另外一个对象时,将另外一个对象标记为灰色,以此满足强三色不变性,不会存在黑色对象引用白色对象。
  • 删除写屏障——在灰色对象删除对白色对象的引用时,将白色对象置为灰色,其实就是快照保存旧的引用关系,这叫STAB(snapshot-at-the-beginning),以此满足弱三色不变性。

三色标记法加混合写屏障机制

v1.8版本之前,运行时会使用插入写屏障保证强三色不变性;

在v1.8中,组合插入写屏障和删除写屏障构成了混合写屏障,保证弱三色不变性;该写屏障会将覆盖的对象标记成灰色(删除写屏障)并在当前栈没有扫描时将新对象也标记成灰色(插入写屏障):写屏障会将被覆盖的指针和新指针都标记成灰色,而所有新建的对象都会被直接标记成黑色。

混合写屏障的具体核心规则如下:

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

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

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

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

GO Gc发展历史

Go V1.3的标记-清除(mark and sweep)法。

Go V1.5的强三色不变式、弱三色不变式、插入屏障、删除屏障。

Go V1.8的三色性+混合写屏障机制。