Golang什么时候会触发GC
By 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