GC触发

  • 内存分配量达到阀值触发 GC

    每次内存分配时,都会检查当前内存分配量是否已达到阀值,如果达到阀值则立即启动 GC:

    • 阀值 = 上次 GC 内存分配量 * 内存增长率
    • 内存增长率由环境变量 GOGC 控制,默认为 100,即每当内存扩大一倍时启动 GC
  • 定期触发 GC 默认情况下,最长 2 分钟,由sysmon触发一次 GC,这个间隔在 src/runtime/proc.go:forcegcperiod 变量中被声明

  • 手动触发 程序代码中也可以使用 runtime.GC()来手动触发 GC。这主要用于 GC 性能测试和统计。

Go 1.3之前的标记-清除(mark and sweep)算法

此算法主要有两个主要的步骤:

  • 标记(Mark phase)
  • 清除(Sweep phase)

第一步,暂停程序业务逻辑, 找出不可达的对象,然后做上标记。第二步,回收标记好的对象。但是有一点需要额外注意:mark and sweep算法在执行的时候,需要程序暂停!即 STW(stop the world)

算法缺点:

  • STW,stop the world;让程序暂停,程序出现卡顿 (重要问题)
  • 标记需要扫描整个heap
  • 清除数据会产生heap碎片

步骤:启动STW - Mark标记 - Sweep清除 - 停止STW

Go 1.3

Go在v1.3版本做了简单的优化,将STW的步骤提前, 减少STW暂停的时间范围,同时并发执行Sweep清除

步骤:启动STW - Mark标记 - 停止STW - Sweep清除

Go 1.5的三色并发标记法、插入写屏障、删除写屏障

三色标记法 实际上就是通过三个阶段的标记来确定清楚的对象都有哪些。

第一步,只要是新创建的对象,默认的颜色都是标记为“白色”。

第二步,每次GC回收开始,然后从根节点开始遍历所有对象(遍历RootSet,非递归形式,只遍历一次),把遍历到的对象从白色集合放入“灰色”集合。

**第三步,遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合。

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

第五步,回收所有的白色标记表的对象,也就是回收垃圾。

没有STW的三色标记法

当以下两个条件同时满足时, 就会出现对象丢失现象!

  • 条件1: 一个白色对象被黑色对象引用**(白色被挂在黑色下)**
  • 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏**(灰色同时丢了该白色)**

为了防止这种现象的发生,最简单的方式就是STW,直接禁止掉其他用户程序对对象引用关系的干扰,但是STW的过程有明显的资源浪费

屏障机制

“强-弱” 三色不变式

  • 强三色不变式

    不存在黑色对象引用到白色对象的指针。

  • 弱三色不变式

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

Dijkstra 插入写屏障

具体操作: 在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色) 满足: 强三色不变式. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色) 伪码如下:

// 添加下游对象
writePointer(slot, ptr):
    // 标记灰色(新下游对象ptr) 
    shade(ptr)
    // 当前下游对象slot = 新下游对象ptr
    *slot=ptr

场景:

A.添加下游对象(nil, B)   // A之前没有下游, 新添加一个下游对象B, B被标记为灰色
A.添加下游对象(C, B)     // A将下游对象C 更换为B,  B被标记为灰色

这段伪码逻辑就是写屏障。我们知道,黑色对象的内存槽有两种位置, 。栈空间的特点是容量小,但是要求相应速度快,因为函数调用弹出频繁使用,所以“插入屏障”机制,在栈空间的对象操作中不使用。而仅仅使用在堆空间对象的操作中(即栈中使用STW暂停保护)。

Yuasa 删除写屏障

具体操作: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。 满足: 弱三色不变式. (保护灰色对象到白色对象的路径不会断)

伪代码:

// 添加下游对象
writePointer(slot, ptr):
    // 如果当前对象是灰色或白色
    if ( isGrey(slot) || isWhite(slot) )
        // 标记灰色(当前下游对象ptr) 
        shade(*slot)
    // 当前下游对象slot = 新下游对象ptr
    *slot = ptr

场景:

A.添加下游对象(B, nil)   // A对象,删除B对象的引用。  B被A删除,被标记为灰(如果B之前为白)
A.添加下游对象(B, C)     // A对象,更换下游B变成C。   B被A删除,被标记为灰(如果B之前为白)

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

  • 插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活。仅适用于堆(程序运行基本在栈中,存在大量变量声明、赋值及函数调用,若栈中使用插入写屏障,将极大增加复杂度、降低性能)
  • 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。

整体流程

img

1. 初始化 GC 任务,包括开启写屏障(write barrier)和开启辅助GC(mutator assist),统计 root对象的任务数量等,这个过程需要STW2. 扫描所有 root 对象(全局指针、goroutine(G) 栈上的指针(扫描对应 G栈时需停止该G)),将其加入灰色队列,并循环处理灰色队列的对象,直到灰色队列为空,该过程后台并行执行 3. 完成标记工作,重新扫描(re-scan)全局指针和栈。因为 Mark是并行执行的,且栈中不适用插入写屏障,所以栈中可能会存在新的未扫描的对象。同时这个re-scan过程会执行STW4. 按照标记结果回收所有的白色对象,该过程后台并行执行。

Go 1.8的混合写屏障(hybrid write barrier)机制

整体流程

img

1. GC开始将栈上的可达对象全部扫描并标记为黑色(当前过程无需STW) 2. GC开始执行标记操作,任何在堆\栈上创建的新对象,均为黑色。 3. 标记结束,开始STW,重新扫描全局指针,不再rescan栈,并执行其他相关操作 4. 关闭STW回收未标记对象,调整下一次GC pacing

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

伪代码:

// 添加下游对象
writePointer(slot, ptr):
    // 标记灰色(当前下游对象ptr) 
    shade(*slot)
    // 如果当前堆栈对象是黑色
    if current stack is grey:
        // 标记灰色(新下游对象ptr) 
        shade(ptr)
    // 当前下游对象slot = 新下游对象ptr
    *slot = ptr

这里我们注意, 屏障技术是不在栈上应用的,因为要保证栈的运行效率。

3个版本Mark&Sweep对比

  • GoV1.3 - 普通标记清除法,整体过程需要启动STW,效率极低。
  • GoV1.5 - 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通
  • GoV1.8 - 三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW,效率较高。

垃圾回收的基本想法