一、垃圾回收
抛开具体的语言,垃圾回收(GC)在计算机科学中解决的核心问题只有一个:对象生命周期的自动化管理 。
如果手动管理内存(如 C/C++ 的
malloc/free),我们面临的是由于"人为疏忽"导致的两个极端错误:
悬挂指针(Dangling
Pointer) :过早释放,导致后续访问出错。
内存泄漏(Memory
Leak) :忘记释放,导致资源耗尽。
GC
的出现,是为了将"判断内存是否不再使用"这个逻辑,从业务代码 剥离,下沉到运行时(Runtime) 。
从大的方面来讲,实现垃圾回收主要是要解决 2 个问题:
怎么判断哪些对象是垃圾?
如何清理垃圾?
1.1 垃圾搜索算法
从原理上讲,一个对象被判定为垃圾,意味着当前程序的后续执行中,再也无法访问到它了 。这在计算机科学中被称为对象存活性(Object
Liveness)问题。
主要有 2 个思路:引用计数法和可达性分析。
1.1.1 引用计数法
给每个对象贴一个计数器。只要有一个地方引用它,计数器就
+1;引用失效(比如指针置空或离开作用域),计数器就
-1。当计数器归零时,该对象即为垃圾。一旦变成垃圾,立刻就能被回收,不需要等待特定的
GC 时间点。
但是存在循环引用 的缺陷:假如对象 A 引用 B,B 也引用
A,除此之外没有其他人引用它们。虽然它们在外部已经无法访问(本质是垃圾),但它们互相揪着对方,计数器永远是
1,导致内存泄漏。
CPython(Python 的解释器)的主力 GC
机制就是引用计数,但它配合了"标记-清除"来专门处理循环引用问题。PHP 和
C++ 的 std::shared_ptr 也是基于此思路。
1.1.2 可达性分析
从根(GC
Roots)节点向下搜索对象节点,搜索走过的路经称为引用链,当一个对象到根之间没有连通的话,则对象不可用。
可以作为 GC Roots
的对象通常是指那些肯定在使用中 的对象:
被栈上的指针引用;
被全局变量的指针引用;
被寄存器中的指针引用;
可达性分析的核心挑战是在遍历过程中,如果程序还在运行(对象引用关系在变),图就在变,怎么保证准确性?传统的做法是
STW (Stop The World) ,暂停所有用户线程专门来做
GC。现代的做法是 三色标记法 (Tri-color Marking) (如 Go
语言),允许 GC
线程和用户线程并发运行,用读写屏障(Barrier)技术来修正并发带来的标记误差,从而尽可能减少
STW 的时长。
1.2 垃圾回收算法
找出了垃圾,下一步就是回收内存。这里的核心矛盾是:效率
vs 空间碎片 。
1.2.1 标记清理法
算法分成 标记 和 清除
两个阶段,先标记出要回收的对象,然后统一回收这些对象。
简单。
效率不高,标记和清除的效率都不高。
标记清除后会产生大量不连续的内存碎片,从而导致在分配大对象时触发
GC。
Go 使用的就是标记清除法
虽然普通的标记清除法会造成内存碎片的问题,但是由于 Go
的内存模型中,将内存天然划分成多个 span,所以不存在内存碎片问题。故 Go
用了这种实现简单的标记清除法。对于 Go 内存模型不熟悉的读者,可参阅:Go
底层原理丨内存模型 。
1.2.2 标记复制法
把内存分成两块完全相同的区域 ,每次使用其中一块,当一块使用完了,就把这块上还存活的对象拷贝到另外一块,然后把这块清除掉。
实现简单、运行高效,不用考虑内存碎片的问题。
内存有些浪费。
JVM 实际实现中,是将内存分为一块较大的 Eden 区和两块较小的 Survivor
空间,每次使用 Eden 和一块 Survivor,回收时,把存活的对象复制到另外一块
Survivor。
HotSpot 默认的 Eden 和 Survivor 比是 8:1,也就是每次能用 90%
的新生代空间。
如果 Survivor
空间不够,就要依赖老年代进行分配担保,把放不下的对象直接进入老年代。
1.2.3 标记整理法
标记过程跟标记清除一样,但后续不是直接清除可回收对象,而是让所有存活对象都向一端移动,然后直接清除边界以外的内存。
标记整理法的开销较大,Java 的老年代就采用标记整理法,因为老年代的 GC
频率较低。
二、宏观概述
在对 GC 有了一个简单的了解之后,我们先来详细了解 Go
语言的垃圾回收机制的宏观详细设计,在下一章节我们将在 AI
的帮助下,深入源码(Go1.25.3)去了解去背后的底层实现细节和那些令人叹为观止的优化思路。
截止 Go1.25,Go 还是使用的三色标记法 + 并发标记清理法 +
混合写屏障 进行垃圾回收,Go 官方透露在 Go1.26 将默认开启 Green
Tea GC,关于 Green Tea GC,将会在下篇进行详细展开。
2.1 核心架构特征
并发标记-清扫 (Concurrent Mark-Sweep)
类型精确 (Type
Accurate):知道内存中哪些是指针
写屏障 (Write Barrier):保证并发标记的正确性
非分代 (Non-generational)
非压缩 (Non-compacting)
Per-P 分配 :减少锁竞争
2.2 三色标记法
2.2.1 基本原理
Go 将对象用三种颜色来进行标记:
黑色 :本对象已经被 GC
访问过,且本对象的子引用对象也已经被访问过了
灰色 :本对象已访问过,但是本对象的子引用对象还没有被访问过,全部访问完会变成黑色,属于中间态
白色 :尚未被GC访问过的对象,如果全部标记已完成依旧为白色的,称为不可达对象,既垃圾对象
2.2.2 基本步骤
起初所有堆上的对象都是【白色】的;
将 GC Roots 直接引用到的对象挪到【灰色】中;
对【灰色】的对象进行根搜索算法:
将该对象引用到的其他对象加入【灰色】中;
将自己挪到【黑色】中;
重复 3 直到【灰色】为空;
回收【白色】中的对象。
2.2.3 删除屏障
并发标记时,对指针释放的白色对象置灰。
这样可以避免在并发 GC 的过程中,由于指针的转移造成对象被误清。
比如一开始 B → C,当 B 在灰色集合的时候,释放了对 C
的指针,但是这个时候有一个在黑色集合的 E 指向了 C,也就是 E → C。由于 E
已经分析过了,所以在对 B 进行分析的时候,就会漏掉 C,导致后面 C
还是在白色集合中,就被误清了。
加入删除屏障后,C 会被强制置灰,就不会误清了。
2.2.4 插入屏障
并发标记时,对指针新指向的白色对象置灰。
这样可以避免在并发 GC 的过程中,误清掉指针新指向的对象。
比如一开始并没有指向 C 的对象,但是在 GC 过程中,E → C,但是由于 E
已经分析过了,已经进入黑色集合了,所以最后会漏掉 C,导致 C 被误清。
加入插入屏障后,C 会被强制置灰,就不会误清了。
2.3 GC 四阶段循环
graph TB
%% 定义样式
classDef stw fill:#ffcdd2,stroke:#c62828,stroke-width:2px,color:#b71c1c;
classDef concurrent fill:#e1f5fe,stroke:#0277bd,stroke-width:2px,color:#01579b;
classDef trigger fill:#fff9c4,stroke:#fbc02d,stroke-dasharray: 5 5,color:#f57f17;
%% 节点定义
subgraph Cycle [GC 循环周期]
direction TB
P1(Phase 1: Sweep Termination 清扫终止):::stw
P2(Phase 2: Concurrent Mark 并发标记):::concurrent
P3(Phase 3: Mark Termination 标记终止):::stw
P4(Phase 4: Concurrent Sweep 并发清扫):::concurrent
end
%% 触发条件
Trigger(GC Trigger 堆阈值/定时/手动):::trigger
%% 连线关系
Trigger --> P1
P1 -->|开启写屏障 SetGCPhase: _GCmark| P2
P2 -->|所有对象标记完成 gcMarkDone| P3
P3 -->|关闭写屏障 SetGCPhase: _GCoff| P4
P4 -->|清理结束 & 等待下一轮| Trigger
%% 补充说明
note1[STW: 准备根对象, 清理上一轮残余] -.-> P1
note2[STW: 保证全局标记完成, 必须全局一致] -.-> P3
2.4 GC 触发机制
堆大小触发 :GOGC=100 时,堆增长 100%
触发(4M→8M)
定时触发 :sysmon 会定时检查,如果 2min 内没有进行
gc,那 runtime 就会进行一次 gc。
手动触发 :runtime.GC()
三、源码解析
结论先行,整个 GC 的全景图如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 ┌─────────────────────────────────────────────────────────────┐ │ GC 周期完整流程 │ └─────────────────────────────────────────────────────────────┘ 触发 GC (gcStart) ├─ 检查触发条件 (gcTrigger.test) │ ├─ gcTriggerHeap: heapLive >= trigger │ ├─ gcTriggerTime: 距上次GC > 2分钟 │ └─ gcTriggerCycle: 手动触发 │ ├─ 完成上一轮扫描 (sweepone) │ └─ === 阶段 1: 扫描终止 (STW) === ├─ stopTheWorld(stwGCSweepTerm) ├─ finishsweep_m() // 完成剩余扫描 ├─ clearpools() // 清理 sync.Pool └─ gcResetMarkState() // 重置标记状态 ┌─────────────────────────────────────────────────────────────┐ │ 阶段 2: 并发标记 │ └─────────────────────────────────────────────────────────────┘ ├─ setGCPhase(_GCmark) // 启用写屏障 ├─ gcBgMarkPrepare() // 准备后台工作者 ├─ gcPrepareMarkRoots() // 准备根对象扫描 ├─ atomic.Store(&gcBlackenEnabled, 1) // 启用标记 └─ startTheWorld() // 恢复世界 并发执行: ├─ 标记工作者 (gcBgMarkWorker) │ ├─ Dedicated Worker: 专用标记 │ ├─ Fractional Worker: 分数标记 │ └─ Idle Worker: 空闲标记 │ ├─ Mutator Assist (gcAssistAlloc) │ └─ 分配者协助标记以保持节奏 │ └─ 根对象扫描 ├─ 扫描所有 goroutine 栈 ├─ 扫描全局变量 └─ 扫描 finalizer 队列 工作循环: └─ while (有灰色对象) { obj = gcw.tryGetObj() // 从队列获取灰色对象 scanobject(obj, gcw) // 扫描对象,标记引用 // 将新发现的灰色对象加入队列 } ┌─────────────────────────────────────────────────────────────┐ │ 阶段 3: 标记终止检测 (gcMarkDone) │ └─────────────────────────────────────────────────────────────┘ 检测循环: ├─ 条件: work.nwait == work.nproc && !gcMarkWorkAvailable │ ├─ === Ragged Barrier === │ └─ forEachP: 刷新所有 P 的本地缓冲 │ ├─ wbBufFlush1(pp) // 写屏障缓冲 │ └─ pp.gcw.dispose() // 工作缓冲 │ ├─ 发现新工作?goto 检测循环 │ └─ === 标记终止 (STW) === ├─ stopTheWorld(stwGCMarkTerm) ├─ 最后检查: 处理 ragged barrier 后的写屏障 ├─ 发现新工作?startTheWorld, goto 检测循环 └─ 确认完成 ┌─────────────────────────────────────────────────────────────┐ │ 阶段 4: 并发扫描 (gcSweep) │ └─────────────────────────────────────────────────────────────┘ ├─ atomic.Store(&gcBlackenEnabled, 0) // 禁用标记 ├─ setGCPhase(_GCoff) // 禁用写屏障 ├─ mheap_.sweepgen += 2 // 更新扫描代数 └─ startTheWorld() // 恢复世界 并发执行: ├─ 后台扫描 (bgsweep) │ └─ 循环调用 sweepone() │ └─ 惰性扫描 (lazy sweep) └─ 分配时按需扫描 span ┌─────────────────────────────────────────────────────────────┐ │ 阶段 5: 等待下次触发 │ └─────────────────────────────────────────────────────────────┘ ├─ 计算下次触发点 │ ├─ heapGoal = heapMarked * (1 + GOGC/100) │ └─ trigger = heapGoal - runway │ └─ 在分配路径检查: heapLive >= trigger └─ 是 → gcStart (回到顶部)
3.1 GC 触发 gcStart()
GC 的触发通过 gcTrigger 机制来检测三种条件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type gcTrigger struct { kind gcTriggerKind now int64 n uint32 } const ( gcTriggerHeap gcTriggerKind = iota gcTriggerTime gcTriggerCycle )
当 gcTrigger.test()
返回 true 时,就会执行 gcStart() 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 }
gcStart()
函数的核心流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 func gcStart (trigger gcTrigger) { mp := acquirem() if gp := getg(); gp == mp.g0 || mp.locks > 1 || mp.preemptoff != "" { releasem(mp) return } releasem(mp) for trigger.test() && sweepone() != ^uintptr (0 ) { } semacquire(&work.startSema) if !trigger.test() { semrelease(&work.startSema) return } gcBgMarkStartWorkers() systemstack(gcResetMarkState) var stw worldStop systemstack(func () { stw = stopTheWorldWithSema(stwGCSweepTerm) }) systemstack(func () { finishsweep_m() }) clearpools() work.cycles.Add(1 ) gcController.startCycle(now, int (gomaxprocs), trigger) setGCPhase(_GCmark) gcBgMarkPrepare() gcPrepareMarkRoots() gcMarkTinyAllocs() atomic.Store(&gcBlackenEnabled, 1 ) systemstack(func () { now = startTheWorldWithSema(0 , stw) }) semrelease(&work.startSema) }
3.2 并发标记 gcBgMarkWorker
在上面 gcStart() 中,会调用 gcBgMarkStartWorkers()
准备后台标记工作者:
1 2 3 4 5 6 7 func gcBgMarkStartWorkers () { ready := make (chan struct {}, 1 ) for gcBgMarkWorkerCount < gomaxprocs { go gcBgMarkWorker(ready) <-ready } }
它的逻辑很简单,就是为每一个 P 调用一个 gcBgMarkWorker(ready) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func gcBgMarkWorker (ready chan struct {}) { ready <- struct {}{} for { systemstack(func () { switch pp.gcMarkWorkerMode { case gcMarkWorkerDedicatedMode: gcDrainMarkWorkerDedicated(&pp.gcw, true ) if gp.preempt { if drainQ := runqdrain(pp); !drainQ.empty() { lock(&sched.lock) globrunqputbatch(&drainQ) unlock(&sched.lock) } } gcDrainMarkWorkerDedicated(&pp.gcw, false ) case gcMarkWorkerFractionalMode: gcDrainMarkWorkerFractional(&pp.gcw) case gcMarkWorkerIdleMode: gcDrainMarkWorkerIdle(&pp.gcw) } casgstatus(gp, _Gwaiting, _Grunning) }) if incnwait == work.nproc && !gcMarkWorkAvailable(nil ) { gcMarkDone() } } }
gcBgMarkWorker() 主要包含 2 个核心逻辑:
根据不同标记的工作者类型调用不同的标记函数,如
gcDrainMarkWorkerDedicated()、gcDrainMarkWorkerFractional()
和 gcDrainMarkWorkerIdle()。而事实上,这 3
个函数,都是调用了 gcDrain()。gcDrain() 函数是
GC
标记阶段的核心工作循环,负责"排空"(drain)标记工作队列,将灰色对象扫描并标记为黑色。这是标记工作者执行实际标记工作的主要函数。
调用层级如下所示:
1 2 3 4 5 6 gcBgMarkWorker (后台工作者) └─> gcDrainMarkWorkerDedicated/Fractional/Idle └─> gcDrain ├─> markroot (扫描根对象) ├─> scanobject (扫描堆对象) └─> scanSpan (扫描 span)
检测标记终止:gcMarkDone(),我们将在 3.3
章节进行详细展开。
3.2.1 标记工作者类型
gcMarkWorkerMode
Go GC 使用三种类型的标记工作者:
gcMarkWorkerDedicatedMode:专用标记工作者,持续标记直到没有更多工作或被抢占。
gcMarkWorkerFractionalMode:分数标记工作者,按照目标使用率工作。
gcMarkWorkerIdleMode:空闲标记工作者,仅在 P
空闲时工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 switch pp.gcMarkWorkerMode {case gcMarkWorkerDedicatedMode: gcDrainMarkWorkerDedicated(&pp.gcw, true ) if gp.preempt { if drainQ := runqdrain(pp); !drainQ.empty() { lock(&sched.lock) globrunqputbatch(&drainQ) unlock(&sched.lock) } } gcDrainMarkWorkerDedicated(&pp.gcw, false ) case gcMarkWorkerFractionalMode: gcDrainMarkWorkerFractional(&pp.gcw) case gcMarkWorkerIdleMode: gcDrainMarkWorkerIdle(&pp.gcw) }
3.2.2 标记工作队列 gcWork
gcWork
是 GC 标记工作的生产者-消费者接口,每个 P 都有自己的
gcWork,通过双缓冲减少全局队列竞争。
1 2 3 4 5 type gcWork struct { wbuf1, wbuf2 *workbuf bytesMarked uint64 flushedWork bool }
它有两个核心方法:
putObj():将一个灰色对象加入工作队列(生产)
tryGetObj():从工作队列取出一个灰色对象(消费)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 func (w *gcWork) putObj(obj uintptr ) { wbuf := w.wbuf1 if wbuf == nil { w.init() wbuf = w.wbuf1 } else if wbuf.nobj == len (wbuf.obj) { w.wbuf1, w.wbuf2 = w.wbuf2, w.wbuf1 wbuf = w.wbuf1 if wbuf.nobj == len (wbuf.obj) { putfull(wbuf) w.flushedWork = true wbuf = getempty() w.wbuf1 = wbuf } } wbuf.obj[wbuf.nobj] = obj wbuf.nobj++ } func (w *gcWork) tryGetObj() uintptr { wbuf := w.wbuf1 if wbuf == nil { w.init() wbuf = w.wbuf1 } if wbuf.nobj == 0 { w.wbuf1, w.wbuf2 = w.wbuf2, w.wbuf1 wbuf = w.wbuf1 if wbuf.nobj == 0 { owbuf := wbuf wbuf = trygetfull() if wbuf == nil { return 0 } putempty(owbuf) w.wbuf1 = wbuf } } wbuf.nobj-- return wbuf.obj[wbuf.nobj] }
设计要点:
双缓冲机制:减少对全局队列的访问频率,降低锁竞争
本地优先:优先使用 P 本地缓冲区,只在必要时访问全局队列
滞后效应:一个缓冲区的容量作为滞后,摊销获取/放回缓冲区的成本
在 gcStart() 的时候,会先执行 gcPrepareMarkRoot()
扫描根对象,即所谓的 GC Roots,如我们前面的可达性分析章节所述, GC Roots
的对象通常是指那些肯定在使用中 的对象:
被栈上的指针引用
被全局变量的指针引用
被寄存器中的指针引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 func gcPrepareMarkRoots () { assertWorldStopped() work.nDataRoots = 0 work.nBSSRoots = 0 for _, datap := range activeModules() { nDataRoots := nBlocks(datap.edata - datap.data) if nDataRoots > work.nDataRoots { work.nDataRoots = nDataRoots } nBSSRoots := nBlocks(datap.ebss - datap.bss) if nBSSRoots > work.nBSSRoots { work.nBSSRoots = nBSSRoots } } mheap_.markArenas = mheap_.heapArenas[:len (mheap_.heapArenas):len (mheap_.heapArenas)] work.nSpanRoots = len (mheap_.markArenas) * (pagesPerArena / pagesPerSpanRoot) work.stackRoots = allGsSnapshot() work.nStackRoots = len (work.stackRoots) work.markrootNext = 0 work.markrootJobs = uint32 (fixedRootCount + work.nDataRoots + work.nBSSRoots + work.nSpanRoots + work.nStackRoots) work.baseData = uint32 (fixedRootCount) work.baseBSS = work.baseData + uint32 (work.nDataRoots) work.baseSpans = work.baseBSS + uint32 (work.nBSSRoots) work.baseStacks = work.baseSpans + uint32 (work.nSpanRoots) work.baseEnd = work.baseStacks + uint32 (work.nStackRoots) }
3.2.4 标记循环 gcDrain()
前面我们提到 gcDrain()
函数是 GC
标记阶段的核心工作循环,负责"排空"(drain)标记工作队列,将灰色对象扫描并标记为黑色。它的核心流程很简单,就是从工作队列中持续取出灰色对象进行扫描,直到满足退出条件 :
工作队列为空
被抢占(如果允许抢占)
满足退出条件(空闲/分数模式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 func gcDrain (gcw *gcWork, flags gcDrainFlags) { checkWork := int64 (1 <<63 - 1 ) var check func () bool if flags&(gcDrainIdle|gcDrainFractional) != 0 { checkWork = initScanWork + drainCheckThreshold if idle { check = pollWork } else if flags&gcDrainFractional != 0 { check = pollFractionalWorkerExit } } if work.markrootNext < work.markrootJobs { for !(gp.preempt && (preemptible || sched.gcwaiting.Load() || pp.runSafePointFn != 0 )) { job := atomic.Xadd(&work.markrootNext, +1 ) - 1 if job >= work.markrootJobs { break } markroot(gcw, job, flushBgCredit) if check != nil && check() { goto done } if goexperiment.GreenTeaGC && gcw.mayNeedWorker { gcw.mayNeedWorker = false if gcphase == _GCmark { gcController.enlistWorker() } } } } for !(gp.preempt && (preemptible || sched.gcwaiting.Load() || pp.runSafePointFn != 0 )) { if work.full == 0 { gcw.balance() } var b uintptr var s objptr if b = gcw.tryGetObjFast(); b == 0 { if s = gcw.tryGetSpan(false ); s == 0 { if b = gcw.tryGetObj(); b == 0 { wbBufFlush() if b = gcw.tryGetObj(); b == 0 { s = gcw.tryGetSpan(true ) } } } } if b != 0 { scanobject(b, gcw) } else if s != 0 { scanSpan(s, gcw) } else { break } if goexperiment.GreenTeaGC && gcw.mayNeedWorker { gcw.mayNeedWorker = false if gcphase == _GCmark { gcController.enlistWorker() } } if gcw.heapScanWork >= gcCreditSlack { gcController.heapScanWork.Add(gcw.heapScanWork) if flushBgCredit { gcFlushBgCredit(gcw.heapScanWork - initScanWork) initScanWork = 0 } checkWork -= gcw.heapScanWork gcw.heapScanWork = 0 if checkWork <= 0 { checkWork += drainCheckThreshold if check != nil && check() { break } } } } done: if gcw.heapScanWork > 0 { gcController.heapScanWork.Add(gcw.heapScanWork) if flushBgCredit { gcFlushBgCredit(gcw.heapScanWork - initScanWork) } gcw.heapScanWork = 0 } }
关键设计点:
工作优先级 :P-local workbuf →
P-local span → 全局 workbuf →
全局 span,优先使用本地缓存,减少全局竞争。
工作平衡 :防止工作集中在某个 P,其他 P 空闲。
抢占检查 :响应抢占请求、STW 请求、forEachP
调用。
信用系统 :后台标记工作产生"信用",Mutator assist
消耗"信用",平衡 GC 工作和应用程序分配。
gcDrain() 包含了 3 个最重要的子逻辑:
markroot(): 标记 GC 的根集(root
set),这些是追踪的起点。
scanobject():扫描一个堆对象,标记它引用的所有对象。
scanSpan():扫描 span,批量处理 span 中的对象,这是
Green Tea GC 的优化,这个我们下一篇再进行展开。
3.2.5 标记根对象 markroot()
关键点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 func markroot (gcw *gcWork, i uint32 , flushBgCredit bool ) int64 { var workDone int64 var workCounter *atomic.Int64 switch { case work.baseData <= i && i < work.baseBSS: workCounter = &gcController.globalsScanWork for _, datap := range activeModules() { workDone += markrootBlock( datap.data, datap.edata-datap.data, datap.gcdatamask.bytedata, gcw, int (i-work.baseData), ) } case work.baseBSS <= i && i < work.baseSpans: workCounter = &gcController.globalsScanWork for _, datap := range activeModules() { workDone += markrootBlock( datap.bss, datap.ebss-datap.bss, datap.gcbssmask.bytedata, gcw, int (i-work.baseBSS), ) } case i == fixedRootFinalizers: for fb := allfin; fb != nil ; fb = fb.alllink { cnt := uintptr (atomic.Load(&fb.cnt)) scanblock(uintptr (unsafe.Pointer(&fb.fin[0 ])), cnt*unsafe.Sizeof(fb.fin[0 ]), &finptrmask[0 ], gcw, nil ) } case i == fixedRootFreeGStacks: systemstack(markrootFreeGStacks) case i == fixedRootCleanups: for cb := (*cleanupBlock)(gcCleanups.all.Load()); cb != nil ; cb = cb.alllink { n := uintptr (atomic.Load(&cb.n)) scanblock(uintptr (unsafe.Pointer(&cb.cleanups[0 ])), n*goarch.PtrSize, &cleanupBlockPtrMask[0 ], gcw, nil ) } case work.baseSpans <= i && i < work.baseStacks: markrootSpans(gcw, int (i-work.baseSpans)) default : workCounter = &gcController.stackScanWork if i < work.baseStacks || work.baseEnd <= i { throw("markroot: bad index" ) } gp := work.stackRoots[i-work.baseStacks] systemstack(func () { userG := getg().m.curg selfScan := gp == userG && readgstatus(userG) == _Grunning if selfScan { casGToWaitingForSuspendG(userG, _Grunning, waitReasonGarbageCollectionScan) } stopped := suspendG(gp) if stopped.dead { gp.gcscandone = true return } if gp.gcscandone { throw("g already scanned" ) } workDone += scanstack(gp, gcw) gp.gcscandone = true resumeG(stopped) if selfScan { casgstatus(userG, _Gwaiting, _Grunning) } }) } if workCounter != nil && workDone != 0 { workCounter.Add(workDone) if flushBgCredit { gcFlushBgCredit(workDone) } } return workDone }
3.2.6 对象扫描 scanobject()
关键点:
Oblet 机制 :大对象(>128KB)被拆分成多个
oblet,每个 ≤128KB
类型指针迭代器 :高效遍历对象中的指针字段,跳过标量字段
快速过滤 :过滤 nil 和自引用,减少不必要的
findObject 调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 func scanobject (b uintptr , gcw *gcWork) { sys.Prefetch(b) s := spanOfUnchecked(b) n := s.elemsize if n == 0 { throw("scanobject n == 0" ) } if s.spanclass.noscan() { throw("scanobject of a noscan object" ) } var tp typePointers if n > maxObletBytes { if b == s.base() { for oblet := b + maxObletBytes; oblet < s.base()+s.elemsize; oblet += maxObletBytes { if !gcw.putObjFast(oblet) { gcw.putObj(oblet) } } } n = s.base() + s.elemsize - b n = min(n, maxObletBytes) tp = s.typePointersOfUnchecked(s.base()) tp = tp.fastForward(b-tp.addr, b+n) } else { tp = s.typePointersOfUnchecked(b) } var scanSize uintptr for { var addr uintptr if tp, addr = tp.nextFast(); addr == 0 { if tp, addr = tp.next(b + n); addr == 0 { break } } scanSize = addr - b + goarch.PtrSize obj := *(*uintptr )(unsafe.Pointer(addr)) if obj != 0 && obj-b >= n { if !tryDeferToSpanScan(obj, gcw) { if obj, span, objIndex := findObject(obj, b, addr-b); obj != 0 { greyobject(obj, b, addr-b, span, gcw, objIndex) } } } } gcw.bytesMarked += uint64 (n) gcw.heapScanWork += int64 (scanSize) if debug.gctrace > 1 { gcw.stats[s.spanclass.sizeclass()].sparseObjsScanned++ } }
3.2.7 对象标记 greyobject()
scanobject() 会将正在扫描的堆对象引用的对象调用
greyobject() 将其从白色标记为灰色。
关键点:
幂等性 :重复标记同一对象是安全的(已标记则直接返回)
原子操作 :标记位和页位图的设置都是原子的,支持并发标记
noscan
优化 :没有指针的对象直接变黑,不入队
预取优化 :将对象预取到缓存,提高后续扫描性能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 func greyobject (obj, base, off uintptr , span *mspan, gcw *gcWork, objIndex uintptr ) { if obj&(goarch.PtrSize-1 ) != 0 { throw("greyobject: obj not pointer-aligned" ) } mbits := span.markBitsForIndex(objIndex) if useCheckmark { if setCheckmark(obj, base, off, mbits) { return } if debug.checkfinalizers > 1 { print (" mark " , hex(obj), " found at *(" , hex(base), "+" , hex(off), ")\n" ) } } else { if mbits.isMarked() { return } mbits.setMarked() arena, pageIdx, pageMask := pageIndexOf(span.base()) if arena.pageMarks[pageIdx]&pageMask == 0 { atomic.Or8(&arena.pageMarks[pageIdx], pageMask) } } if span.spanclass.noscan() { gcw.bytesMarked += uint64 (span.elemsize) return } sys.Prefetch(obj) if !gcw.putObjFast(obj) { gcw.putObj(obj) } }
3.2.8 并发标记小节
gcDrain
的核心逻辑是一个消费循环。它从本地或全局的工作缓冲区(gcWork)中提取指针(灰色对象),并调用
scanobject
对其进行处理。其工作流可以形式化为以下几个步骤:
本地获取(Local Fetch) :首先尝试从当前 P 的本地
gcWork
缓存中获取工作。这是一个无锁操作(Lock-free),效率极高。
全局获取与窃取(Global Fetch &
Steal) :如果本地缓存为空,gcDrain
必须尝试从全局队列获取工作,或者从其他 P
的本地队列中窃取工作。这一步涉及到跨 P 的协调,是锁竞争的高发区。
扫描与着色(Scan &
Shade) :对获取到的每一个对象调用
scanobject,识别其引用的子对象,并通过
greyobject 将子对象加入工作队列(即着色为灰色)。
抢占检查(Preemption
Check) :为了保证调度的公平性,gcDrain
会周期性地检查是否需要让出 P。
整个 gcDrain() 的标记循环流程可以总结为如下图所示:
graph LR
A[灰色对象队列] -->|取出| B[gcDrain]
B --> C[扫描函数]
C -->|markroot| D[扫描根]
C -->|scanobject| E[扫描对象]
C -->|scanSpan| F[扫描Span]
D --> G[greyobject]
E --> G
F --> G
G -->|白→灰| H[设置标记位]
H -->|入队| A
style B fill:#e1f5ff,stroke:#0277bd,stroke-width:3px
style G fill:#ffebee,stroke:#c62828,stroke-width:3px
style A fill:#fff9c4,stroke:#f57f17,stroke-width:2px
3.3 标记终止检测 gcMarkDone()
为了进入并发清理阶段,需要先确保所有标记已经终止,即 Mark
Termination。这是最复杂的阶段,Go 使用分布式终止算法 和
Ragged Barrier 来确保所有标记工作完成。
所谓检测并发标记阶段是否完成,即确认所有可达对象都已标记,没有遗漏的灰色对象 。
在并发环境中,标记工作分散在多个位置:
P-local buffers:每个 P 的 gcWork 缓冲区
Global work queues:全局工作队列 work.full
Write barrier buffers:写屏障缓冲区 wbBuf
Root scan jobs:根对象扫描任务
那么问题就来了:如何在不停止世界的情况下,确保检查所有缓冲区时,不会有新的工作产生?
[!IMPORTANT]
gcMarkDone()
通过"检查所有工作者空闲(nwait==nproc)且全局队列为空 → Ragged
Barrier 同步刷新所有 P 的写屏障缓冲和工作队列到全局 → STW
后验证写屏障无残留工作 "的三步循环检测,任一步骤发现新的灰色对象就回到起点重新检测,直到确认不存在任何隐藏的本地工作和灰色对象后才进入标记终止阶段。
下面是 gcMarkDone() 的源码解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 func gcMarkDone () { semacquire(&work.markDoneSema) top: if !(gcphase == _GCmark && work.nwait == work.nproc && !gcMarkWorkAvailable(nil )) { semrelease(&work.markDoneSema) return } semacquire(&worldsema) work.strongFromWeak.block = true gcMarkDoneFlushed = 0 forEachP(waitReasonGCMarkTermination, func (pp *p) { wbBufFlush1(pp) pp.gcw.dispose() if pp.gcw.flushedWork { atomic.Xadd(&gcMarkDoneFlushed, 1 ) pp.gcw.flushedWork = false } }) if gcMarkDoneFlushed != 0 { semrelease(&worldsema) goto top } now := nanotime() work.tMarkTerm = now getg().m.preemptoff = "gcing" systemstack(func () { stw = stopTheWorldWithSema(stwGCMarkTerm) }) restart := false systemstack(func () { for _, p := range allp { wbBufFlush1(p) if !p.gcw.empty() { restart = true break } } }) if restart { getg().m.preemptoff = "" systemstack(func () { now := startTheWorldWithSema(0 , stw) work.pauseNS += now - stw.startedStopping }) semrelease(&worldsema) goto top } atomic.Store(&gcBlackenEnabled, 0 ) gcWakeAllAssists() gcController.endCycle(now, int (gomaxprocs), work.userForced) gcMarkTermination(stw) }
这里再简单解释一下 Ragged Barrier :
Ragged Barrier
是分布式系统中的一个同步原语,名字来源于它的行为特征:不同处理器/线程到达屏障的时间是"参差不齐"(ragged)的。
用一句话来解释就是 Ragged Barrier
是一种异步同步原语,让多个处理单元独立完成各自的本地状态刷新操作,无需等待其他单元,最终达到全局状态一致的目的。
在并发标记完成检测时,通过 Ragged Barrier 将所有 P
的本地缓冲区(写屏障缓冲和工作队列)刷新到全局,使隐藏的工作可见,从而能够正确判断是否真的没有剩余标记工作。
3.4 并发清理 gcSweep()
标记完成后,进入扫描阶段,gcSweep()
负责初始化和启动垃圾回收的扫描(清理)阶段,将未标记的对象回收,准备下一个
GC 周期。
gcSweep() 可以概括为:
递增 sweepgen(+2) :建立新旧 GC 周期的边界
选择执行模式 :同步立即完成 vs 并发后台进行
启动扫描机制 :直接调用 sweepone() 或唤醒
bgsweep
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 func gcSweep (mode gcMode) bool { assertWorldStopped() if gcphase != _GCoff { throw("gcSweep being done but phase is not GCoff" ) } lock(&mheap_.lock) mheap_.sweepgen += 2 sweep.active.reset() mheap_.pagesSwept.Store(0 ) mheap_.sweepArenas = mheap_.heapArenas mheap_.reclaimIndex.Store(0 ) mheap_.reclaimCredit.Store(0 ) unlock(&mheap_.lock) sweep.centralIndex.clear() if !concurrentSweep || mode == gcForceBlockMode { lock(&mheap_.lock) mheap_.sweepPagesPerByte = 0 unlock(&mheap_.lock) for _, pp := range allp { pp.mcache.prepareForSweep() } for sweepone() != ^uintptr (0 ) { } prepareFreeWorkbufs() for freeSomeWbufs(false ) { } mProf_NextCycle() mProf_Flush() return true } lock(&sweep.lock) if sweep.parked { sweep.parked = false ready(sweep.g, 0 , true ) } unlock(&sweep.lock) return false }
其中 sweepgen 是一个单调递增的计数器,用于追踪
span 的扫描状态,通过设置全局的
mheap_.sweepgen,可以巧妙区分不同状态的
span,从而避免重复扫描。
1 2 3 4 5 6 7 8 9 10 sweepgen 的三种状态(对于当前 sweepgen = N): span.sweepgen = N-2 → 未扫描(unswept) span.sweepgen = N-1 → 正在扫描中 span.sweepgen = N → 已扫描(swept) 通过 +2 递增,巧妙地区分了三个状态: - 当前周期的未扫描:sweepgen - 2 - 当前周期的已扫描:sweepgen - 正在扫描:sweepgen - 1(CAS 操作时的中间状态)
有两种扫描方式,分别是同步扫描和并发扫描,并发扫描实际上执行的是
bgsweep(),它们俩的核心逻辑都在
sweepone(),sweepone() 用于扫描单个
span:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 func sweepone () uintptr { gp := getg() gp.m.locks++ sl := sweep.active.begin() if !sl.valid { gp.m.locks-- return ^uintptr (0 ) } npages := ^uintptr (0 ) var noMoreWork bool for { s := mheap_.nextSpanForSweep() if s == nil { noMoreWork = sweep.active.markDrained() break } if state := s.state.get(); state != mSpanInUse { continue } if s, ok := sl.tryAcquire(s); ok { npages = s.npages if s.sweep(false ) { mheap_.reclaimCredit.Add(npages) } else { npages = 0 } break } } sweep.active.end(sl) if noMoreWork { scavenger.ready() } gp.m.locks-- return npages }
核心逻辑都在 s.sweep(false)
中,它的核心职责是回收未标记的对象,准备 span
给下次分配使用 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 func (sl *sweepLocked) sweep(preserve bool ) bool { s := sl.mspan sweepgen := mheap_.sweepgen if state := s.state.get(); state != mSpanInUse || s.sweepgen != sweepgen-1 { throw("mspan.sweep: bad span state" ) } hadSpecials := s.specials != nil siter := newSpecialsIter(s) for siter.valid() { objIndex := uintptr (siter.s.offset) / size p := s.base() + objIndex*size mbits := s.markBitsForIndex(objIndex) if !mbits.isMarked() { hasFinAndRevived := false for tmp := siter.s; tmp != nil && uintptr (tmp.offset) < endOffset; tmp = tmp.next { if tmp.kind == _KindSpecialFinalizer { mbits.setMarkedNonAtomic() hasFinAndRevived = true break } } if hasFinAndRevived { for siter.valid() && uintptr (siter.s.offset) < endOffset { special := siter.s p := s.base() + uintptr (special.offset) if special.kind == _KindSpecialFinalizer || special.kind == _KindSpecialWeakHandle { siter.unlinkAndNext() freeSpecial(special, unsafe.Pointer(p), size) } else { siter.next() } } } else { for siter.valid() && uintptr (siter.s.offset) < endOffset { special := siter.s p := s.base() + uintptr (special.offset) siter.unlinkAndNext() freeSpecial(special, unsafe.Pointer(p), size) } } } else { if siter.s.kind == _KindSpecialReachable { special := siter.unlinkAndNext() (*specialReachable)(unsafe.Pointer(special)).reachable = true freeSpecial(special, unsafe.Pointer(p), size) } else { siter.next() } } } if s.freeindex < s.nelems { obj := uintptr (s.freeindex) if (*s.gcmarkBits.bytep(obj/8 ) &^ *s.allocBits.bytep(obj/8 ))>>(obj%8 ) != 0 { s.reportZombies() } for i := obj/8 + 1 ; i < divRoundUp(uintptr (s.nelems), 8 ); i++ { if *s.gcmarkBits.bytep(i) &^ *s.allocBits.bytep(i) != 0 { s.reportZombies() } } } s.allocBits = s.gcmarkBits s.gcmarkBits = newMarkBits(uintptr (s.nelems)) if s.pinnerBits != nil { s.refreshPinnerBits() } s.refillAllocCache(0 ) atomic.Store(&s.sweepgen, sweepgen) if spc.sizeclass() != 0 { if nfreed > 0 { s.needzero = 1 gcController.totalFree.Add(int64 (nfreed) * int64 (s.elemsize)) } if !preserve { if nalloc == 0 { mheap_.freeSpan(s) return true } if nalloc == s.nelems { mheap_.central[spc].mcentral.fullSwept(sweepgen).push(s) } else { mheap_.central[spc].mcentral.partialSwept(sweepgen).push(s) } } } else if !preserve { if nfreed != 0 { gcController.totalFree.Add(int64 (size)) mheap_.freeSpan(s) return true } mheap_.central[spc].mcentral.fullSwept(sweepgen).push(s) } return false }
处理流程可参考下图进行理解:
flowchart TD
Start([sweep 入口]) --> Verify[验证状态 sweepgen == global-1]
Verify --> Specials[处理 Specials]
Specials --> FinCheck{有 finalizer?}
FinCheck -->|是| Revive[复活对象 加入执行队列]
FinCheck -->|否| FreeSp[释放 specials]
Revive --> Zombie
FreeSp --> Zombie
Zombie[检查僵尸对象] --> ZombieCheck{存在?}
ZombieCheck -->|是| Error[throw]
ZombieCheck -->|否| Core
Core[核心: 位图交换]:::highlight
Core --> Swap["allocBits = gcmarkBits gcmarkBits = new()"]:::highlight
Swap --> Update[更新 sweepgen global-1 → global]
Update --> Classify[归类 span]
Classify --> CheckN{nalloc?}
CheckN -->|0| ToHeap[freeSpan 归还堆]
CheckN -->|nelems| ToFull[fullSwept 完全占满]
CheckN -->|其他| ToPartial[partialSwept 部分占用]
ToHeap --> RetTrue[return true]
ToFull --> RetFalse[return false]
ToPartial --> RetFalse
RetTrue --> End([结束])
RetFalse --> End
Error --> End
classDef highlight fill:#ffeb3b,stroke:#f57c00,stroke-width:3px
style Start fill:#4caf50,color:#fff
style End fill:#4caf50,color:#fff
3.5 计算下次触发点
GC 结束时,通过 pacer 计算下次触发点:
简单来说,Pacer 通过测量上次 GC
的分配速率和扫描速率,计算出一个合适的触发点(Trigger),让 GC
既不会太频繁(浪费
CPU),也不会太晚(OOM),实现自适应的垃圾回收调度。
3.6 STW 分析
graph TB
%% 定义样式
classDef stw fill:#ffcdd2,stroke:#c62828,stroke-width:2px,color:#b71c1c;
classDef concurrent fill:#e1f5fe,stroke:#0277bd,stroke-width:2px,color:#01579b;
classDef trigger fill:#fff9c4,stroke:#fbc02d,stroke-dasharray: 5 5,color:#f57f17;
%% 节点定义
subgraph Cycle [GC 循环周期]
direction TB
P1(Phase 1: Sweep Termination 清扫终止):::stw
P2(Phase 2: Concurrent Mark 并发标记):::concurrent
P3(Phase 3: Mark Termination 标记终止):::stw
P4(Phase 4: Concurrent Sweep 并发清扫):::concurrent
end
%% 触发条件
Trigger(GC Trigger 堆阈值/定时/手动):::trigger
%% 连线关系
Trigger --> P1
P1 -->|开启写屏障 SetGCPhase: _GCmark| P2
P2 -->|所有对象标记完成 gcMarkDone| P3
P3 -->|关闭写屏障 SetGCPhase: _GCoff| P4
P4 -->|清理结束 & 等待下一轮| Trigger
%% 补充说明
note1[STW: 准备根对象, 清理上一轮残余] -.-> P1
note2[STW: 保证全局标记完成, 必须全局一致] -.-> P3
我们再来看一下这张图,分析一下为什么 ① ③ 阶段需要 STW,而 ② ④
却不需要呢?
Sweep Termination - STW
1 2 3 4 5 6 需要做的事: ├─ 完成上一轮剩余的扫描 ├─ 清理 sync.Pool ├─ 重置标记状态 (gcResetMarkState) ├─ 启用写屏障 (setGCPhase(_GCmark)) └─ 准备根对象扫描
必须 STW 的核心原因 :
写屏障必须同时在所有 P 上生效
如果不 STW,某些 P 开启了写屏障,某些还没开
会导致指针写入不一致,漏标记对象 ❌
Mark Termination - STW
1 2 3 4 5 6 需要做的事: ├─ 禁用 workers 和 assists ├─ 刷新缓存 (mcache flush) ├─ 禁用写屏障 ├─ 切换阶段 (setGCPhase(_GCoff)) └─ 启动清扫 (gcSweep)
必须 STW 的核心原因 :
需要全局一致性视图 :确认所有标记工作真的完成了
禁用写屏障必须原子 :不能有些 P
关了,有些还开着
位图状态切换 :sweepgen += 2
需要在稳定状态下进行
Mark Phase - 并发
为什么可以并发?
1 2 3 4 5 6 7 有写屏障保护: mutator 写指针 → 写屏障记录 → 标记为灰色 workers 并发标记 → 不会漏标记对象 ✓ 三色不变式保证正确性: - 强三色:黑色对象不能直接指向白色对象 - 弱三色:黑色→白色之间必有灰色对象
关键技术 :
写屏障 :Dijkstra 插入屏障,拦截所有指针写入
并发安全 :标记位操作是原子的
增量处理 :每个 worker 独立工作,不需要全局同步
Sweep Phase - 并发
为什么可以并发?
1 2 3 4 5 6 7 扫描和分配互不干扰: ├─ 扫描:检查 span.sweepgen,CAS 获取所有权 ├─ 分配:检查 span.sweepgen,只用已扫描的 span └─ sweepgen 机制保证不会重复扫描 ✓ 惰性扫描: 分配时按需扫描,保证使用的 span 都是干净的
关键技术 :
sweepgen 版本控制 :每个 span 有独立状态
CAS 操作 :原子获取扫描所有权
按需扫描 :分配路径自动扫描,不阻塞其他操作
对比总结
[!IMPORTANT]
Sweep Termination 和 Mark Termination 需要 STW
是因为必须原子地切换写屏障状态和确认全局一致性,而 Mark Phase 和 Sweep
Phase 可以并发是因为有写屏障和 sweepgen 机制保护,不需要全局同步。
本质 :STW
用于状态切换 ,并发用于实际工作 。🎯
阶段
STW
原因
时长
Sweep Termination
✋ 是
同步启用写屏障
~100μs
Mark Phase
✅ 否
写屏障保护
~数十ms
Mark Termination
✋ 是
全局一致性确认
~100μs
Sweep Phase
✅ 否
sweepgen + CAS
~数十ms
3.7 核心机制总结
三色标记法 :白色(未扫描)→ 灰色(已发现)→
黑色(已扫描)
混合写屏障 :Dijkstra + Yuasa
保证并发标记的正确性
分布式终止检测 :Ragged Barrier
确保所有本地缓冲区都被刷新
Mutator
Assist :分配速度过快时,分配者协助标记以保持 GC进 度
代数机制 :sweepgen 通过 +2
的方式区分不同 GC 周期的 span 状态
整个 GC
周期是一个精密设计的并发系统,在保证程序正确性的同时,最大化地减少 STW
时间,实现了低延迟的垃圾回收。
四、工程建议
4.1 参数调优
4.1.1 GOGC 参数
GOGC 控制 GC 的激进程度:
GOGC 值
含义
效果
GOGC=off
禁用 GC
内存会无限增长
GOGC=50
堆增长 50% 触发
频繁 GC,低内存使用
GOGC=100
堆增长 100% 触发(默认)
平衡
GOGC=200
堆增长 200% 触发
低频 GC,高内存使用
GOGC=400
堆增长 400% 触发
极低频 GC,极高内存
4.1.2 GOMEMLIMIT
Go1.19 新增的软内存限制,优先级高于 GOGC,GOMEMLIMIT 让
Go 程序知道"不能超过多少内存",接近时自动加大 GC 力度,既防止 OOM
又提高内存利用率,是容器化部署的必备配置。
建议配置为:GOMEMLIMIT = 容器限制 × 0.9。
4.2 性能优化
减少分配
1 2 3 4 5 6 7 8 9 10 11 for i := 0 ; i < n; i++ { s := fmt.Sprintf("%d" , i) } var buf bytes.Bufferfor i := 0 ; i < n; i++ { buf.Reset() fmt.Fprintf(&buf, "%d" , i) }
对象池复用
1 2 3 4 5 6 7 8 9 10 var bufPool = sync.Pool{ New: func () interface {} { return new (bytes.Buffer) }, } buf := bufPool.Get().(*bytes.Buffer) defer bufPool.Put(buf)buf.Reset()
预分配切片
1 2 3 4 5 6 7 8 9 10 11 var s []int for i := 0 ; i < 10000 ; i++ { s = append (s, i) } s := make ([]int , 0 , 10000 ) for i := 0 ; i < 10000 ; i++ { s = append (s, i) }
避免指针密集结构
1 2 3 4 5 6 7 8 9 10 11 type Node struct { Value *int Next *Node } type Node struct { Value int Next *Node }
栈分配优先
1 2 3 4 5 6 7 8 9 10 11 func bad () *int { x := 42 return &x } func good () int { x := 42 return x }
4.3 分析工具
go tool pprof
go tool trace
go build -gcflags -m
GODEBUG="gctrace=1"
以下面程序为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport ( "net/http" "sync" _ "net/http/pprof" ) func main () { go func () { wg := sync.WaitGroup{} wg.Add(10 ) for i := 0 ; i < 10 ; i++ { go func (wg *sync.WaitGroup) { var counter int for i := 0 ; i < 1e10 ; i++ { counter++ } wg.Done() }(&wg) } wg.Wait() }() _ = http.ListenAndServe(":8080" , nil ) }