Golang什么时候会触发GC
admin
- 4 minutes read - 653 wordsGolang采用了三色标记法来进行垃圾回收,那么在什么场景下会触发这个GC动作呢?
源码主要位于文件 [src/runtime/mgc.go](https://github.com/golang/go/blob/go1.16/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()](https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go#L1286-L1463) 函数,它有一个 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](https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go#L1232-L1240) 的参数来开始的,而手动触发指定的条件值为 [gcTriggerCycle](https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go#L1253-L1256)。gcStart是一个很复杂的函数,有兴趣的可以看一下源码实现。
// 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 的值有三种,分别为 [gcTriggerHeap](https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go#L1243-L1246)、 [gcTriggerTime](https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go#L1248-L1251) 和 [gcTriggerCycle](https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go#L1253-L1256)。
- gcTriggerHeap当前分配的内存达到一定阈值时触发,这个阈值在每次GC过后都会根据堆内存的增长情况和CPU占用率来调整;主要是- mallocgc()函数,其中分配内存对象大小又分多种情况,建议看下源码实现。
- gcTriggerTime自从上次GC后间隔时间达到了- [runtime.forcegcperiod](https://github.com/golang/go/blob/go1.16/src/runtime/proc.go#L5089-L5094)时间(默认为2分钟),将启动GC;主要是- [sysmon](https://github.com/golang/go/blob/go1.16/src/runtime/proc.go#L5235-L5243)监控线程
- gcTriggerCycle如果当前没有开启垃圾收集,则启动GC;主要是调用函数- [runtime.GC()](https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go#L1123-L1197)
运行时会通过 [gcTrigger.test()](https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go#L1259-L1284) 函数判断是否需要触发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()](https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go#L812-L953) 函数。
以上是触发GC的三个条件,那么Golang 以是如何实现这种机制的呢?
其实 runtime 在程序启动时,会在一个初始化函数 init() 里启用一个 forcegchelper() 函数,这个函数位于 [proc.go](https://github.com/golang/go/blob/go1.16/src/runtime/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()](https://github.com/golang/go/blob/go1.16/src/runtime/proc.go#L339-L343) 函数主动让自己陷入休眠,然后由 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()](https://github.com/golang/go/blob/go1.16/src/runtime/proc.go#L5096-L5250) 会在一个 for 语句里一直判断这个gcTriggerTime 这个条件是否满足,如果满足的话,会将 forcegc.g 这个 goroutine 添加到全局队列里进行调度(这里 forcegc 是一个全局变量)。
调度器在调度循环 runtime.schedule 中还可以通过垃圾收集控制器的 runtime.gcControllerState.findRunnabledGCWorker 获取并执行用于后台标记的任务。
参考资料
- https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/
- https://zhuanlan.zhihu.com/p/74853110