Go 内存回收-GC 源码1-触发与阶段

内存回收源码(一):触发与阶段

总览

Go 运行时垃圾回收主要落在 runtime/mgc.gomgcmark.gomgcsweep.gombarrier.go 等。本系列拆成四篇,便于按调用链阅读:

文件 内容
本文 何时 触发:gcTrigger 三种路径;gcStart 与一轮 GC 的 阶段 I~IV (阶段 II 只保留要点,不含 Worker / Assist 源码展开)
内存回收源码(2)-并发标记与Assist 并发标记里的 后台 WorkerAssistgcAssistAlloc 链)、gcDrainNgcParkAssist
内存回收源码(3)-三色写屏障与清扫 三色gcDrain / scanobject / greyobject混合写屏障并发清扫sweepone
内存回收源码(4)-清扫 清扫阶段的 mcentralsweeponemspan.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 :后台循环里批量调用 sweeponeruntime/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 等 ...
	}
}
相关推荐
shining2 小时前
[Golang]Eino探索之旅-初窥门径
后端
掘金者阿豪2 小时前
Mac 程序员效率神器:6 个我每天都在用的 Mac 工具推荐(Alfred / Paste / PixPin / HexHub / iTerm2 /)
后端
小村儿2 小时前
连载04-CLAUDE.md ---一起吃透 Claude Code,告别 AI coding 迷茫
前端·后端·ai编程
han_hanker2 小时前
springboot 一个请求的顺序解释
java·spring boot·后端
2501_921649492 小时前
原油期货量化策略开发:历史 K 线获取、RSI、MACD 布林带计算到多指标共振策略回测
后端·python·金融·数据分析·restful
杰克尼2 小时前
SpringCloud_day05
后端·spring·spring cloud
ServBay3 小时前
阿里超强编程模型Qwen 3.6 -Plus 发布,国产编程AI的春天?
后端·ai编程
用户8356290780513 小时前
使用 Python 自动生成 Excel 柱状图的完整指南
后端·python
希望永不加班3 小时前
SpringBoot 静态资源访问(图片/JS/CSS)配置详解
java·javascript·css·spring boot·后端