美文网首页
golang笔记——GC 原理

golang笔记——GC 原理

作者: 无昵称啊 | 来源:发表于2022-07-11 22:35 被阅读0次

    一、GC触发

    • 内存分配量达到阀值触发 GC
      每次内存分配时,都会检查当前内存分配量是否已达到阀值,如果达到阀值则立即启动 GC:
      • 阀值 = 上次 GC 内存分配量 * 内存增长率
      • 内存增长率由环境变量 GOGC 控制,默认为 100,即每当内存扩大一倍时启动 GC
    • 定期触发 GC
      默认情况下,最长 2 分钟,由sysmon触发一次 GC,这个间隔在 src/runtime/proc.go:forcegcperiod 变量中被声明
    • 手动触发
      程序代码中也可以使用 runtime.GC()来手动触发 GC。这主要用于 GC 性能测试和统计。

    二、 v1.3 标记-清除算法

    1、 v1.3 之前

    整体流程图


    具体步骤
    1. 启动STW(Stop The World),暂停程序业务逻辑


    2. 从根对象开始标记,找出所有可达的对象,并做上标记。如下图所示:


    3. 标记完成后,清除未标记的对象


    4. 停止STW,让程序继续运行。然后循环重复这个过程,直到process程序生命周期结束。

    缺点:

    • STW让程序暂停,CPU全部用于垃圾回收,程序出现卡顿(重要问题)
    • 标记需要扫描整个heap
    • 清楚数据会产生heap碎片
    2、 v1.3优化Mark & Sweep

    由于未被标记的不可达对象,基本不会再次被引用(由于程序中没有对象拥有不可达对象的地址,所以难以再被其他对象引用)。因此,在Mark标记完成后,就停止STW,让程序恢复运行,可以减少STW的时间,同时也不会影响Sweep清除的正确性。

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

    优化后的GC仍存在STW的问题,Go V1.5版本使用了三色并发标记法来继续优化这个问题。

    三、v1.5 三色并发标记、插入写屏障、删除写屏障

    三色标记的一个明显好处是能够让用户程序和 mark 并发的进行

    “三色”只是为了叙述上方便抽象出来的一种说法,实际上对象并没有颜色之分。这里的“三色”,对应了垃圾回收过程中对象的三种状态:

    • 黑色:已被回收器访问到的对象,其子对象也已被被回收器访问到
    • 灰色:已被回收器访问到的对象,但其可能仍存在子对象未被回收器访问到。
    • 白色:未被回收器访问到的对象(潜在的垃圾),其内存可能会被垃圾收集器回收。

    1、三色并发标记--具体步骤:

    1. 初始将所有内存标记为白色,将所有对象放入白色集合中


    2. 然后将 roots 加入worklist(进入worklist即被视为变成灰色)


    3. 从根节点开始遍历所有对象,把遍历到的对象从白色集合放入“灰色”集合。
      (本次遍历只会对根节点下的子节点进行1次遍历,是非递归遍历,仅遍历1层)


    4. 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合。
      (这一次遍历只扫描灰色对象,将灰色对象的第一层遍历可抵达的对象由白色变为灰色, 将灰色对象标记为黑色,并将其从灰色标记表移动到黑色标记表)


    5. 重复第3步, 直到灰色标记表中无任何对象



    6. 回收所有的白色标记表的对象.


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

    2、没有STW,带来漏标问题

    假设我们执行三色并发标记时,不执行STW。用户程序,有可能将1个灰色对象G下白色子对象W的引用,转移给1个黑色对象B。在GC时会误删除对象W,从而导致程序异常。详见下图:


    由上图可以看出,有两种情况,在三色标记法中,是不希望被发生的:
    - 条件1: 一个白色对象被黑色对象引用(白色被挂在黑色下)
    - 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了该白色)

    如果当以上两个条件同时满足时,就会出现对象丢失现象!为了防止这种现象的发生,我们只要使用一种机制,尝试去破坏上面的两个必要条件就可以了。这样也可以避免STW带来的资源浪费问题。

    3、屏障机制

    破坏上面的两个必要条件,有两种方式:

    • 强三色不变式:不允许黑色对象引用任何白色对象
    • 弱三色不变式:允许黑色对象引用白色对象,但该白色对象的可达路径中必须存在灰色对象

    在GC源码中对应两种屏障机制:“Dijkstra 插入写屏障”、“Yuasa 删除写屏障”。

    • Dijkstra 插入写屏障
      具体操作:在A对象引用C对象的时候,C对象被标记为灰色。(将C挂在A下游,C必须被标记为灰色)

      满足:强三色不变式. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色)
      伪码如下
    // 添加下游对象
    writePointer(slot, ptr):
        // 标记灰色(新下游对象ptr) 
        shade(ptr)
        // 当前下游对象slot = 新下游对象ptr
        *slot=ptr
    
    • Yuasa 删除写屏障
      具体操作:从对象B被删除的对象C,如果对象C自身为灰色或者白色,那么对象C被标记为灰色。

      满足:弱三色不变式. (保护灰色对象到白色对象的路径不会断)
      伪码如下
    // 添加下游对象
    writePointer(slot, ptr):
        // 如果当前对象是灰色或白色
        if ( isGrey(slot) || isWhite(slot) )
            // 标记灰色(当前下游对象ptr) 
            shade(*slot)
        // 当前下游对象slot = 新下游对象ptr
        *slot = ptr
    
    • 插入写屏障与删除写屏障的缺点

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


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

    四、v1.8 混合写屏障

    Go V1.8版本引入了混合写屏障机制(hybrid write barrier),避免了对栈re-scan的过程,极大的减少了STW的时间。结合了两者的优点。
    整体流程

    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
    

    通过以下场景,我们看下在v1.5和v1.8中的GC变化

    v1.5 v1.8
    堆对象A --x--> 堆对象B,栈对象C ----> 堆对象B 堆中删除写屏障标记B为灰色 堆中删除写屏障标记B为灰色
    堆对象A ----> 新堆对象B 堆中插入写屏障标记B为灰色 堆中创建的新对象默认均为黑色
    栈对象A --x--> 栈对象B,栈对象C ----> 栈对象B rescan后,最终标记B为黑色 GC开始时,已将B标记为黑色
    栈对象A ----> 新栈对象B rescan后,最终标记B为黑色 栈中创建的新对象默认均为黑色
    栈对象A ----> 新堆对象B rescan后,最终标记B为黑色 堆中创建的新对象默认均为黑色
    栈对象A --x--> 栈对象B rescan后,最终被清除 GC开始时,已将B标记为黑色;等待下次GC清除

    五、3个版本Mark&Sweep对比

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

    六、GC Sweep


    Go 提供2种方式来清理内存:

    1. 在后台启动一个 worker 等待清理内存,一个一个mspan 处理
      当开始运行程序时,Go 将设置一个后台运行的Worker(唯一的任务就是去清理内存),它将进入睡眠状态并等待内存段扫描
      当GC worker未清理完内存,但新一轮GC又开始了。这时这个运行 GC 的 goroutine 就会在开始标记阶段前去协助完成剩余的清理工作
    2. 当申请分配内存时候 lazy 触发
      当应用程序 goroutine 尝试在堆内存中分配新内存时,会触发该操作。清理导致的延迟和吞吐量降低被分散到每次内存分配时
      该方式属于即时执行,由于被使用的内存段已经被分发到每一个P 的本地缓存 mcache 中,很难追踪首先清理哪些内存,因此Go 会先将所有内存段移动到mcentral,让本地缓存mcache 再次请求它们,去即时清理。即时扫描确保所有内存段都会得到清理(节省资源),同时不会阻塞程序执行

    Referencs:
    https://www.yuque.com/aceld/golang/zhzanb
    https://www.cnblogs.com/zj420255586/p/14261834.html#12-%E6%A0%87%E8%AE%B0-%E6%B8%85%E9%99%A4
    https://golang.design/under-the-hood/zh-cn/part2runtime/ch08gc/basic/
    https://www.qycn.com/xzx/article/10803.html
    https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html
    https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part2-semantics.html
    https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part3-semantics.html

    相关文章

      网友评论

          本文标题:golang笔记——GC 原理

          本文链接:https://www.haomeiwen.com/subject/kxxvbrtx.html