Golang什么时候会触发GC

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

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

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

不管哪种触发方式,底层回收机制是一样的,所以我们先看一下手动触发,看看能否根据它来找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过后都会根据堆内存的增长情况和CPU占用率来调整;主要是 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
}

对于 memstats.gc_trigger 变量的值是动态进行调整的,见 gcSetTriggerRatio() 函数。

以上是触发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 获取并执行用于后台标记的任务。

参考资料