22 December 2019

参考文章:垃圾回收器

因为三色标记以及混合写屏障在 Go 中的源码实现,笔者目前尚未理解清楚,所以本文省略了该部分内容。后续有时间研究明白了再补上~

并发三色标记垃圾回收

算法思想可参考:Golang’s Real-time GC in Theory and PracticeGolang’s realtime garbage collector

然而在实际实现上却有些变化,例如网上很多文章都说在启用写屏障的情况下,新创建的对象都标记为灰色,但笔者在 go 1.13 源码中,func gcmarknewobject(obj, size, scanSize uintptr)方法被 mallocgc 调用,其注释写明了:gcmarknewobject marks a newly allocated object black. obj must not contain any non-nil pointers。

Go 运行时的垃圾回收分四个阶段:

  1. 准备阶段:STW,初始化标记任务,启用写屏障
  2. 标记阶段 GCMark:标记存活对象,并发与用户代码执行,保持只占用25%CPU
  3. 标记终止阶段 GCMarkTermination:STW,关闭写屏障
  4. 清扫阶段 GCOff:回收白色对象,并发与用户代码执行

GC 初始化

在 Go 运行时初始化 schedinit() 方法中会调用 gcinit()。

func gcinit() {
    if unsafe.Sizeof(workbuf{}) != _WorkbufSize {
        throw("size of Workbuf is suboptimal")
    }

    // No sweep on the first cycle.
    mheap_.sweepdone = 1

    // Set a reasonable initial GC trigger.
    memstats.triggerRatio = 7 / 8.0

    // Fake a heap_marked value so it looks like a trigger at
    // heapminimum is the appropriate growth from heap_marked.
    // This will go into computing the initial GC goal.
    // 初始化计算 GC 目标所需要的参数
    memstats.heap_marked = uint64(float64(heapminimum) / (1 + memstats.triggerRatio))

    // Set gcpercent from the environment. This will also compute
    // and set the GC trigger and goal.
    _ = setGCPercent(readgogc())

    work.startSema = 1
    work.markDoneSema = 1
}

func setGCPercent(in int32) (out int32) {
    // Run on the system stack since we grab the heap lock.
    systemstack(func() {
        lock(&mheap_.lock)
        out = gcpercent
        if in < 0 {
            in = -1
        }
        gcpercent = in
        // 计算触发GC的最小堆大小,默认4M
        heapminimum = defaultHeapMinimum * uint64(gcpercent) / 100
        // Update pacing in response to gcpercent change.
        // 计算触发GC的阈值,GC目标,标记的步调,清扫的步调,释放内存回操作系统的步调
        gcSetTriggerRatio(memstats.triggerRatio)
        unlock(&mheap_.lock)
    })

    // If we just disabled GC, wait for any concurrent GC mark to
    // finish so we always return with no GC running.
    if in < 0 {
        gcWaitOnMark(atomic.Load(&work.cycles))
    }

    return out
}

gcSetTriggerRatio 中关于 GC 相关参数的计算:

  • gcpercent:百分比参数,用于调整 GC 目标(memstats.next_gc)和触发阈值(memstats.triggerRatio),默认为100
  • memstats.heap_marked:上一轮 GC 结束后,还在使用的堆内存
  • memstats.heap_alive:从 mcentral 分配出去的小对象堆内存 + mheap.free 分配出去的大对象堆内存
  • memstats.next_gc:当下一次 GC 结束后,堆内存的目标,max(minTrigger, memstats.heap_marked*(1+memstats.triggerRatio), memstats.heap_marked*(1+gcpercent/100))
  • memstats.gc_trigger:触发 GC 的堆内存阈值,max(minTrigger, memstats.heap_marked*(1+memstats.triggerRatio))
  • memstats.triggerRatio:初始化为 min(7/8, 0.95*gcpercent/100),即0.875,会在每次 GC 标记结束时被 gcControllerState.endCycle 更新

如果当前正在执行 GC,gcSetTriggerRatio 会调用 gcController.revise 调整标记的步调,如果 memstats.heap_live <= memstats.next_gc,则 gcAssistAlloc 需要做的工作 scanWork 也会越少,反之越多。

gcSetTriggerRatio 还会设置 mheap_.sweepPagesPerByte,当 memstats.heap_live 越接近 memstats.gc_trigger,mheap_.sweepPagesPerByte 的值越大,则在创建新对象时,需要清扫的工作越多,反之越少。

gcSetTriggerRatio 还会调用 gcPaceScavenger,判断是否达到阈值(memstats.heap_sys - memstats.heap_released > 1.1 * memstats.next_gc),如果达到,则计算 mheap_.scavengeBytesPerNS,并唤醒 scavenge 协程,最终调用 mheap.scavengeLocked,释放 mheap.free 中空闲的 span。

GC 开始

触发 GC 的时机

GC 并发标记

辅助标记是为了防止 GC 期间,程序不断分配大量内存,导致 GC 标记时间过长或者无法结束标记。

GC 标记结束

GC 并发清扫

清扫过程是为了回收白色对象,辅助清扫是为了避免下次GC启动时做过多的清扫工作。

GC 并发释放内存回操作系统