内存回收源码(一):触发与阶段
总览
Go 运行时垃圾回收主要落在 runtime/mgc.go、mgcmark.go、mgcsweep.go、mbarrier.go 等。本系列拆成四篇,便于按调用链阅读:
| 篇 | 文件 | 内容 |
|---|---|---|
| 一 | 本文 | 何时 触发:gcTrigger 三种路径;gcStart 与一轮 GC 的 阶段 I~IV (阶段 II 只保留要点,不含 Worker / Assist 源码展开) |
| 二 | 内存回收源码(2)-并发标记与Assist | 并发标记里的 后台 Worker 、Assist (gcAssistAlloc 链)、gcDrainN、gcParkAssist |
| 三 | 内存回收源码(3)-三色写屏障与清扫 | 三色 与 gcDrain / scanobject / greyobject、混合写屏障 、并发清扫 与 sweepone |
| 四 | 内存回收源码(4)-清扫 | 清扫阶段的 mcentral 、sweepone 、mspan.sweep(回收槽位、释放 span) |
一条主线:触发 → STW 准备 → 并发标记(mutator + worker + assist + 写屏障)→ 标记收尾 STW → 并发 sweep 。四篇顺序读,可沿分配(mallocgc)与标记(gcDrain)对照源码。
GC触发时机
一轮 GC 会不会开,在源码里统一抽象成 gcTrigger 三种:gcTriggerHeap(堆涨到控制器的阈值)、gcTriggerTime(距上次 GC 太久)、gcTriggerCycle(手动或非堆逻辑要求「从某一周期开始跑完整一轮」)。是否满足条件由 gcTrigger.test() 判断,且只有当前处于 _GCoff(典型含义是「没在正式标记那一套阶段里」)且允许 GC 时才会为真。
三种 kind 的定义与 test 分支如下(摘自 runtime/mgc.go):
go
// 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
}
const (
gcTriggerHeap gcTriggerKind = iota
gcTriggerTime
gcTriggerCycle
)
func (t gcTrigger) test() bool {
if !memstats.enablegc || panicking.Load() != 0 || gcphase != _GCoff {
return false
}
switch t.kind {
case gcTriggerHeap:
trigger, _ := gcController.trigger()
return gcController.heapLive.Load() >= trigger
case gcTriggerTime:
if gcController.gcPercent.Load() < 0 {
return false
}
lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))
return lastgc != 0 && t.now-lastgc > forcegcperiod
case gcTriggerCycle:
return int32(t.n-work.cycles.Load()) > 0
}
return true
}
1. 堆阈值触发
分配路径里在合适的时机会检查「堆触发」。满足条件就 gcStart。例如 mallocgc 末尾一段:
go
if checkGCTrigger {
if t := (gcTrigger{kind: gcTriggerHeap}); t.test() {
gcStart(t)
}
}
heapLive 与控制器的 trigger 在具体算法里算,对照阅读时可打开 gcController.trigger()(一般在 pacer 相关源码里)。
2. 时间间隔触发(sysmon 加 forcegc 协程)
最长间隔由 forcegcperiod 给出,默认约 2 分钟;注释写明「超过这么久没做 GC 就强行走一次」(摘自 runtime/proc.go):
go
// forcegcperiod is the maximum time in nanoseconds between garbage
// collections. If we go this long without a garbage collection, one
// is forced to run.
var forcegcperiod int64 = 2 * 60 * 1e9
sysmon 里用 gcTriggerTime 调用 test(),并在 forcegc 帮手处于 idle 时把它唤醒去跑 GC(摘自 runtime/proc.go):
go
if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && forcegc.idle.Load() {
lock(&forcegc.lock)
forcegc.idle.Store(false)
var list gList
list.push(forcegc.g)
injectglist(&list)
unlock(&forcegc.lock)
}
被唤醒的 forcegchelper 里会 gcStart(gcTriggerTime)(摘自 runtime/proc.go):
go
gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()})
3. 手动 runtime.GC()
对外 API 要保证调用线程等到「一整轮」走完。实现上先 gcWaitOnMark 对齐当前进度,再用 gcTriggerCycle 启动目标周期,继续等待标记结束并协助扫完(摘自 runtime/mgc.go):
go
func GC() {
// ... 注释:一轮含 sweep termination、mark、mark termination、sweep ...
n := work.cycles.Load()
gcWaitOnMark(n)
gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1})
gcWaitOnMark(n + 1)
for work.cycles.Load() == n+1 && sweepone() != ^uintptr(0) {
Gosched()
}
// ... 其后等待 isSweepDone、堆 profile 等 ...
}
以上三块覆盖了最常见的三种「谁在什么路径上调用 gcStart」。
一轮 GC 的阶段
在两次极短的 STW(开启和结束标记)之间,通过三色标记、写屏障与辅助回收(Assist)实现与用户程序并行的内存扫描,并在标记完成后进入并发清扫阶段。
I. 准备阶段(STW)
- 触发 :堆分配、
sysmon定时、runtime.GC()等,路径不同但最终都会调到gcStart(见本章第一节)。 - 动作:停世界(STW)。
- 任务 :完成上一轮尚未完成的清扫(sweep termination);把
gcphase切到_GCmark并打开写屏障;把全局、各 G 的栈等根入队。 - 结束 :
startTheWorldWithSema,进入并发标记。
gcStart 里与上述顺序对应的一段(runtime/mgc.go,中间已删与统计、调试相关的行):
go
systemstack(func() {
stw = stopTheWorldWithSema(stwGCSweepTerm)
})
systemstack(func() {
finishsweep_m()
})
setGCPhase(_GCmark)
gcBgMarkPrepare()
gcPrepareMarkRoots()
gcMarkTinyAllocs()
atomic.Store(&gcBlackenEnabled, 1)
// ... acquirem、累计 STW 停顿时间 ...
systemstack(func() {
now = startTheWorldWithSema(0, stw)
})
setGCPhase 里会根据是否为 _GCmark / _GCmarktermination 打开写屏障。
go
func setGCPhase(x uint32) {
atomic.Store(&gcphase, x)
writeBarrier.enabled = gcphase == _GCmark || gcphase == _GCmarktermination
}
II. 并发标记阶段(Concurrent Mark)
- 动作:并发运行。业务逻辑(Mutator)与 GC 任务(Worker)共同占用 CPU。
- 关键 :
- Mutator:照常执行业务的 goroutine;会 分配、改写指针槽
- Worker:每个 P 上挂的
gcBgMarkWorker,专门负责寻找并标记存活对象。 - 写屏障(Write Barrier):由于业务代码在跑,指针会变。写屏障像监控一样,记录下所有指针改动,确保不会漏掉任何存活对象。
- 辅助回收 (Assist):一种"配额制度"。分配内存过快的协程会被强制拉去做标记工作,防止内存增速跑赢回收速度。
- 结束 : worker/assist 干完活,会有路径进入
gcMarkDone,做分布式收尾再 STW。
Worker、Assist的源码与步骤见 第二篇:内存回收源码(2)-并发标记与Assist。
III. 标记收尾阶段(STW)
- 动作 :进入
_GCmarktermination,在系统栈上跑gcMark等收尾扫描。 - 任务 :确认标记阶段结束;随后
gcphase设回_GCoff,关闭写屏障 ,调用gcSweep为并发清扫铺路;再更新 pacing(如gcControllerCommit)供下一轮触发阈值使用。 - 结束 :
gcMarkTermination内部会再启世界(细节在同函数后半段)。
阶段切换与关屏障、进入 sweep(runtime/mgc.go,函数前半与中间若干行已省略):
go
func gcMarkTermination(stw worldStop) {
setGCPhase(_GCmarktermination)
// ... acquirem、CAS 把当前 G 切成等待 GC 等 ...
systemstack(func() {
gcMark(startTime)
})
systemstack(func() {
work.heap2 = work.bytesMarked
setGCPhase(_GCoff)
stwSwept = gcSweep(work.mode)
})
// ... trace、更新 memstats、gcControllerCommit、startTheWorld ...
}
说明:gcController.endCycle 在调用 gcMarkTermination 之前 已在 gcMarkDone 里执行过;gcMarkTermination 里还有 gcControllerCommit 等,专门把 pacing 落到下一周期要用的状态。
IV. 并发清扫阶段(Concurrent Sweep)
- 语义 :
gcphase为_GCoff,写屏障关闭;新分配对象为白,需要时分配器会先清扫 span。 - bgsweep :后台循环里批量调用
sweepone(runtime/mgcsweep.go)。 - 懒惰清扫:分配路径在需要 span 时也会清扫,与 bgsweep 并行把堆扫干净。
go
func bgsweep(c chan int) {
// ...
for {
const sweepBatchSize = 10
nSwept := 0
for sweepone() != ^uintptr(0) {
nSwept++
if nSwept%sweepBatchSize == 0 {
goschedIfBusy()
}
}
// ... freeSomeWbufs、gopark 等 ...
}
}