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 的参数来开始的,而手动触发指定的条件值为 gcTriggerCycle。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、 gcTriggerTime 和 gcTriggerCycle。
-
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 获取并执行用于后台标记的任务。