0
点赞
收藏
分享

微信扫一扫

Golang什么时候会触发GC

闲云困兽 2021-09-30 阅读 64
日记本

Golang采用了三色标记法来进行垃圾回收,那么在什么场景下会触发这个回收动作呢?

源码主要位于文件 src/runtime/mgc.go go version 1.16

触发条件从大方面说,可分为 手动触发 和 系统触发 两种方式。手动触发一般很少用,主要由开发者通过调用 runtime.GC() 函数来实现,而对于系统自动触发是 运行时 根据一些条件判断来进行的,这也正是本文要介绍的内容。

不管哪种触发方式,底层回收机制是一样的,所以我们先看一下手动触发,根据它来找系统触发的条件。

// src/runtime/mgc.go

// GC runs a garbage collection and blocks the caller until the
// garbage collection is complete. It may also block the entire
// program.
func GC() {
    n := atomic.Load(&work.cycles)
    gcWaitOnMark(n)

    // We're now in sweep N or later. Trigger GC cycle N+1, which
    // will first finish sweep N if necessary and then enter sweep
    // termination N+1.
    // 触发GC
    gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1})

    // Wait for mark termination N+1 to complete.
    gcWaitOnMark(n + 1)

    ......
}

可以看到开始执行GC的是 gcStart() 函数,它有一个 gcTrigger 参数,是一个触发条件结构体,它的结构体也很简单。

// gcStart starts the GC. It transitions from _GCoff to _GCmark (if
// debug.gcstoptheworld == 0) or performs all of GC (if
// debug.gcstoptheworld != 0).
//
// This may return without performing this transition in some cases,
// such as when called on a system stack or with locks held.
func gcStart(trigger gcTrigger) {
    ......

    for trigger.test() && sweepone() != ^uintptr(0) {
        sweep.nbgsweep++
    }

    // Perform GC initialization and the sweep termination
    // transition.
    semacquire(&work.startSema)
    // Re-check transition condition under transition lock.
    if !trigger.test() {
        semrelease(&work.startSema)
        return
    }

    ......

    gcBgMarkStartWorkers()
    ......
    clearpools()

    work.cycles++

    gcController.startCycle()
    work.heapGoal = memstats.next_gc

    ......
}

其实在Golang 内部所有的GC都是通过 gcStart() 函数,然后指定一个gcTrigger 的参数来开始的,而手动触发指定的条件值为 gcTriggerCyclegcStart是一个很复杂的函数,有兴趣的可以看一下源码实现。

// A gcTrigger is a predicate for starting a GC cycle. Specifically,
// it is an exit condition for the _GCoff phase.
type gcTrigger struct {
    kind gcTriggerKind
    now  int64  // gcTriggerTime: current time
    n    uint32 // gcTriggerCycle: cycle number to start
}

type gcTriggerKind int

const (
    // gcTriggerHeap indicates that a cycle should be started when
    // the heap size reaches the trigger heap size computed by the
    // controller.
    gcTriggerHeap gcTriggerKind = iota

    // gcTriggerTime indicates that a cycle should be started when
    // it's been more than forcegcperiod nanoseconds since the
    // previous GC cycle.
    gcTriggerTime

    // gcTriggerCycle indicates that a cycle should be started if
    // we have not yet started cycle number gcTrigger.n (relative
    // to work.cycles).
    gcTriggerCycle
)

对于 kind 的值有三种,分别为gcTriggerHeapgcTriggerTimegcTriggerCycle

  • gcTriggerHeap 堆内存的分配的大小达到了控制器计算的大小,将启动GC;主要是 mallocgc() 函数,其中分析内存对象大小又分多种情况,建议看下源码实现。
  • gcTriggerTime 自从上次GC后间隔时间达到了runtime.forcegcperiod 时间(默认为2分钟),将启动GC;主要是 sysmon 监控线程
  • gcTriggerCycle 如果当前没有开启垃圾收集,则启动GC;主要是 runtime.GC()

运行时会通过 gcTrigger.test() 函数来决定是否需要触发GC,只要满足上面基中一个即可。

// test reports whether the trigger condition is satisfied, meaning
// that the exit condition for the _GCoff phase has been met. The exit
// condition should be tested when allocating.
func (t gcTrigger) test() bool {
    if !memstats.enablegc || panicking != 0 || gcphase != _GCoff {
        return false
    }
    switch t.kind {
    case gcTriggerHeap:
        // 堆内存不足
        // Non-atomic access to heap_live for performance. If
        // we are going to trigger on this, this thread just
        // atomically wrote heap_live anyway and we'll see our
        // own write.
        return memstats.heap_live >= memstats.gc_trigger
    case gcTriggerTime:
        if gcpercent < 0 {
            return false
        }
        // 大于2分钟
        lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))
        return lastgc != 0 && t.now-lastgc > forcegcperiod
    case gcTriggerCycle:
        // t.n > work.cycles, but accounting for wraparound.
        return int32(t.n-work.cycles) > 0
    }
    return true
}

到此我们基本明白了这三种触发GC的条件,那么对于系统自动触发这种,Golang 从一个程序的开始到运行,它又是如何一步一步监控到这个条件的呢?

其实 runtime 在程序启动时,会在一个初始化函数 init() 里启用一个 forcegchelper() 函数,这个函数位于 proc.go 文件。

// start forcegc helper goroutine
func init() {
    go forcegchelper()
}

func forcegchelper() {
    forcegc.g = getg()
    lockInit(&forcegc.lock, lockRankForcegc)
    for {
        lock(&forcegc.lock)
        if forcegc.idle != 0 {
            throw("forcegc: phase error")
        }
        atomic.Store(&forcegc.idle, 1)
        goparkunlock(&forcegc.lock, waitReasonForceGCIdle, traceEvGoBlock, 1)
        // this goroutine is explicitly resumed by sysmon
        // 这个goroutine 是由 sysmon 唤醒恢复
        if debug.gctrace > 0 {
            println("GC forced")
        }
        // Time-triggered, fully concurrent.
        gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()})
    }
}

为了减少系统资源占用,在 forcegchelper 函数里会通过 goparkunlock() 函数主动让自己陷入休眠,以后由 sysmon() 监控线程根据条件来恢复这个gc goroutine。

func sysmon() {
    ......

    for {
        // check if we need to force a GC
        // 超过2分钟
        if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {
            lock(&forcegc.lock)
            forcegc.idle = 0
            var list gList
            list.push(forcegc.g)
            injectglist(&list)
            unlock(&forcegc.lock)
        }
    }

    ......

}

可以看到 sysmon() 会在一个 for 语句里一直判断这个gcTriggerTime 这个条件是否满足,如果满足的话,会将 forcegc.g 这个goroutine添加到全局队列里进行调度(这里 forcegc 是一个全局变量)。

调度器在调度循环 runtime.schedule 中还可以通过垃圾收集控制器的 runtime.gcControllerState.findRunnabledGCWorker 获取并执行用于后台标记的任务。

参考资料

举报

相关推荐

0 条评论