背景

正好周末时间,就打算梳理以下自己对go gc的理解。跳出语言层面来说,gc分为两种,一种是手动创建,手动销毁。另一种就是由自动分配自动销毁,前者就是c,c++的代表,后者就是java,go。

而整个流程来说的话,程序运行->内存分配->垃圾回收,至于内存分配,这个我留到下一篇来讲解梳理以下。

其中内存分配需要依靠内存分配器,而垃圾回收需要垃圾回收器,垃圾回收器有不同的垃圾回收算法。

go语言中的25个关键字 go语言的gc_java

垃圾回收算法

  • 引用技术算法。主要是在程序启动后,每个对象初始标记为0,如果有别的对象引用该对象,那么引用技术就+1,但是存在一个问题就是循环引用的问题。这个和java中的类似。
  • (追踪式垃圾回收)可达性分析算法,初始标记一些root对象,根据这些root对象 递归遍历,如果可达的话,那么就标记为可用,否则剩余对象就是可以被清除的对象。
    Go 现在用的三色标记法就属于追踪式垃圾回收算法的一种。

根对象

根对象在垃圾回收的术语中又叫做根集合,它是垃圾回收器在标记过程时最先检查的对象,包括:

全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。
寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。

在GC的标记阶段首先需要标记的就是"根对象", 从根对象开始可到达的所有对象都会被认为是存活的。
根对象包含了全局变量, 各个G的栈上的变量等, GC会先扫描根对象然后再扫描根对象可到达的所有对象。

Mark & Sweep

  • STW(Stop the World): 需要暂停Mutatur,来确定所有的引用关系,而这部分也就是优化GC的重点,因为STW对于系统的吞吐量来说是非常致命的,所以有些业务场景下是不可接受的。
  • Mark & Sweep :该过程就是先STW,然后用root 根对象去标记可达的对象,先标记后清除,如下图,先通过根对象标记到A B C D G,然后清除掉E F。

go 1.1 GC流程

  • Stop the World
  • Mark:通过 Root 和 Root 直接间接访问到的对象, 来寻找所有可达的对象,并进行标记。
  • Sweep:对堆对象迭代,已标记的对象置位标记。所有未标记的对象加入freelist, 可用于再分配。
  • Start the Wrold

总结:先停止程序,然后标记 清除,然后在程序正常运行。其实缺点非常明显,这种对业务影响是非常大的,分配内存慢,内存碎片高

go 1.3 gc流程

我们分析一下,其实在清除阶段,只要不去干扰对象引用关系,其实不会对最终清除的对象有影响。所以mark阶段可以STW,Sweep阶段可以并发执行。所以1.3的流程就是 mark阶段STW,并发Sweep。从一定程度上可以减少STW对程序业务的影响。

go 1.5 gc (三色标记法)

1.5版本在标记过程中使用三色标记法。标记和清扫都并发执行的,但标记阶段的前后需要 STW 一定时间来做 GC 的准备工作和栈的re-scan。

什么是三色标记?

三色标记是对标记清除法的改进,标记清除法在整个执行时要求长时间 STW,Go 从1.5版本开始改为三色标记法,初始将所有内存标记为白色,然后将 roots 加入待扫描队列(进入队列即被视为变成灰色),然后使用并发 goroutine 扫描队列中的指针,如果指针还引用了其他指针,那么被引用的也进入队列,被扫描的对象视为黑色。

白色对象:潜在的垃圾,其内存可能会被垃圾收集器回收。

黑色对象:活跃的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象,垃圾回收器不会扫描这些对象的子对象。

灰色对象 :活跃的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象。

go语言中的25个关键字 go语言的gc_golang_02


其实直接看图的话,可以非常明显的明白,初始为白色,然后将root可达的对象标记为灰色,如果这个对象被被的对象引用那么标记为黑色,到最后如果不可达对象 必定是白色,那么可以被清除。

写屏障和删屏障

1.5版本在标记过程中使用三色标记法。回收过程主要有四个阶段,其中,标记和清扫都并发执行的,但标记阶段的前后需要 STW 一定时间来做GC 的准备工作和栈的 re-scan。
使用并发的垃圾回收,也就是多个 Mutator 与 Mark 并发执行,想要在并发或者增量的标记算法中保证正确性,我们需要达成以下两种三色不变性(Tri-color invariant)中的任意一种:
强三色不变性:黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象。
弱三色不变性 :黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径。
总结:其实本上强三色和弱三色 主要就是为了保证在系统和标记对象的过程中,不会造成对对象引用关系链造成影响。

写屏障
写屏障的目的其实为了保证在系统执行和标记对象并发执行的之后,保证系统的正确性。

writePointer(slot, ptr):
    shade(ptr)
    *slot = ptr

写屏障保证在将黑色对象对象引用到白色对象时,(这个时候不满足强三色规则)会将白色对象标记为灰色对象,满足强三色规则。如下图中,A对象引用到C,将C对象标记为灰色,然后A可达C,所以A C D设置为黑色。

go语言中的25个关键字 go语言的gc_golang_03


问题:虽然写屏障可以保证强三色不变式,但是Go中仅针对堆上的对象进行写屏障,而栈中对象写屏障是比较耗费性能的,所以要么通过栈上写屏障保证或者通过STW来重新扫描保证对象变黑来保证。

删屏障

删除屏障也是拦截写操作的,但是是通过保护灰色对象到白色对象的路径不会断来实现的。也就是若三色不变式。

writePointer(slot, ptr):
    if (isGery(slot) || isWhite(slot))
        shade(*slot)
    *slot = ptr

go语言中的25个关键字 go语言的gc_go语言中的25个关键字_04

混合屏障

插入写屏障和删除写屏障的短板:

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

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

具体操作:
1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW);
2、GC期间,任何在栈上创建的新对象,均为黑色;
3、被删除的对象标记为灰色;
4、被添加的对象标记为灰色;

总结

1.GC的过程 标记清除,但是关键点在于对STW的合理配合下不影响系统的正常运行,由此引入写屏障和删屏障操作来保证强三色和弱三色规则,但是本上他们存在一定的问题,所以使用混合屏障来保证。