04-垃圾回收器全链路
章节概述
Go 语言的垃圾回收器采用三色标记算法,结合写屏障机制实现并发垃圾回收。本章将深入解析 GC 的设计原理、源码实现和优化策略,帮助读者理解 Go 如何实现低延迟的垃圾回收。
学习目标
- 理解三色标记算法的原理
- 掌握写屏障机制的作用
- 了解并发标记和清扫的实现
- 学会 GC 调优和性能分析
- 能够识别和解决 GC 相关问题
️ GC 架构概览
三色标记模型
┌─────────────────────────────────────────────────────────┐
│ 三色标记模型 │
├─────────────────────────────────────────────────────────┤
│ 白色 (White) 灰色 (Gray) 黑色 (Black) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 未访问对象 │ │ 已发现未扫描│ │ 已扫描完毕 │ │
│ │ 候选垃圾 │ │ 待处理队列 │ │ 存活对象 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
GC 阶段划分
┌─────────────────────────────────────────────────────────┐
│ GC 执行阶段 │
├─────────────────────────────────────────────────────────┤
│ 1. 标记开始 (STW) 2. 并发标记 3. 标记结束 (STW) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 暂停所有 G │ │ 与用户并发 │ │ 重新扫描 │ │
│ │ 扫描根对象 │ │ 标记对象 │ │ 完成标记 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 4. 并发清扫 │
│ ┌─────────────┐ │
│ │ 清理垃圾对象│ │
│ │ 回收内存 │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
核心数据结构
GC 状态定义
文件位置:src/runtime/mgc.go
const (
_GCoff = iota // GC 关闭
_GCmark // 标记阶段
_GCmarktermination // 标记结束阶段
_GCsweep // 清扫阶段
)
type gcController struct {
// GC 触发条件
triggerRatio float64
heapGoal uint64
heapLive uint64
heapMarked uint64
// 工作统计
scanWork int64
bgScanCredit int64
// 辅助标记
assistTime int64
dedicatedMarkWorkersNeeded int64
}
标记工作结构
type gcWork struct {
wbuf1, wbuf2 *workbuf
bytesMarked uint64
scanWork int64
flushedWork bool
}
type workbuf struct {
workbufhdr
obj [(_WorkbufSize - unsafe.Sizeof(workbufhdr{})) / sys.PtrSize]uintptr
}
type workbufhdr struct {
node lfnode // 必须是第一个字段
nobj int
}
GC 触发机制
触发条件
func (c *gcController) trigger() gcTrigger {
// 1. 堆大小触发
if c.heapLive >= c.heapGoal {
return gcTriggerHeap
}
// 2. 时间触发
if c.heapLive >= c.heapMarked+c.heapMarked/4 {
return gcTriggerTime
}
// 3. 手动触发
if c.heapLive >= c.heapMarked+c.heapMarked/2 {
return gcTriggerManual
}
return gcTriggerNone
}
触发参数
// GOGC 参数控制
func (c *gcController) update() {
// 计算触发比例
c.triggerRatio = float64(gcpercent) / 100.0
// 计算目标堆大小
c.heapGoal = c.heapMarked + uint64(float64(c.heapMarked)*c.triggerRatio)
// 更新统计信息
c.heapLive = atomic.Load64(&c.heapLive)
c.heapMarked = atomic.Load64(&c.heapMarked)
}
三色标记算法
标记过程
func gcMark() {
// 1. 初始化标记
gcMarkRoots()
// 2. 并发标记
for {
// 从灰色队列获取对象
obj := gcWork.tryGet()
if obj == nil {
break
}
// 扫描对象
scanObject(obj)
}
// 3. 完成标记
gcMarkDone()
}
func scanObject(obj uintptr) {
// 获取对象类型
typ := getType(obj)
// 扫描对象字段
for i := 0; i < typ.numField; i++ {
field := obj + typ.fieldOffset[i]
if isPointer(field) {
// 标记引用的对象
markObject(*(*uintptr)(unsafe.Pointer(field)))
}
}
}
根对象标记
func gcMarkRoots() {
// 1. 标记全局变量
for _, v := range globalVars {
markObject(v)
}
// 2. 标记栈变量
for _, g := range allgs {
markStack(g)
}
// 3. 标记寄存器
markRegisters()
}
func markStack(g *g) {
// 扫描 goroutine 栈
for sp := g.stack.lo; sp < g.stack.hi; sp += sys.PtrSize {
if isPointer(sp) {
markObject(*(*uintptr)(unsafe.Pointer(sp)))
}
}
}
️ 写屏障机制
写屏障原理
写屏障是保证并发 GC 正确性的关键机制,当程序修改指针时,写屏障会检查并标记新引用的对象。
// 写屏障函数
func gcWriteBarrier(dst *uintptr, src uintptr) {
if writeBarrierEnabled {
if src != 0 {
// 标记源对象
markObject(src)
}
}
*dst = src
}
写屏障类型
1. 插入写屏障 (Insert Barrier)
func insertBarrier(dst *uintptr, src uintptr) {
if src != 0 {
// 标记新引用的对象
markObject(src)
}
*dst = src
}
2. 删除写屏障 (Delete Barrier)
func deleteBarrier(dst *uintptr, old uintptr) {
if old != 0 {
// 标记被删除引用的对象
markObject(old)
}
*dst = 0
}
写屏障实现
文件位置:src/runtime/mbarrier.go
func gcWriteBarrier(dst *uintptr, src uintptr) {
if writeBarrierEnabled {
if src != 0 {
// 检查是否需要标记
if src < gcController.heapMarked {
// 对象在标记开始前分配,需要标记
markObject(src)
}
}
}
*dst = src
}
并发标记实现
标记工作器
func gcMarkWorker() {
for {
// 获取标记工作
work := gcWork.tryGet()
if work == nil {
// 没有工作,休眠
gopark(nil, nil, waitReasonGCMarkWorker, traceEvGoBlock, 0)
continue
}
// 执行标记工作
scanObject(work)
// 更新统计
atomic.AddInt64(&gcController.scanWork, 1)
}
}
辅助标记
func gcAssistAlloc(gp *g) {
// 计算需要辅助的工作量
assistWork := gp.gcAssistBytes
if assistWork > 0 {
// 执行辅助标记
for assistWork > 0 {
obj := gcWork.tryGet()
if obj == nil {
break
}
scanObject(obj)
assistWork--
}
}
}
并发清扫
清扫过程
func gcSweep() {
// 1. 清扫 span
for {
s := mheap_.sweepSpans[0].pop()
if s == nil {
break
}
// 清扫 span
if s.sweep() {
// span 已清空,放回 mcentral
mheap_.central[s.spanclass].mcentral.cacheSpan()
}
}
// 2. 清扫大对象
for _, obj := range mheap_.largeObjects {
if !obj.marked {
// 释放大对象
mheap_.freeLarge(obj)
}
}
}
增量清扫
func (s *mspan) sweep() bool {
// 清扫对象
for i := s.freeindex; i < s.nelems; i++ {
if !s.allocBits.isMarked(i) {
// 对象未标记,加入空闲列表
s.freeList.push(s.base() + i*s.elemsize)
}
}
// 更新 span 状态
s.allocCount = s.freeList.len()
s.freeindex = s.nelems
return s.allocCount == 0
}
️ 实战代码
1. GC 状态监控
package main
import (
"fmt"
"runtime"
"time"
)
func monitorGC() {
var m runtime.MemStats
for {
runtime.ReadMemStats(&m)
fmt.Printf("=== GC 状态监控 ===\n")
fmt.Printf("堆大小: %d KB\n", m.HeapAlloc/1024)
fmt.Printf("堆目标: %d KB\n", m.NextGC/1024)
fmt.Printf("GC 次数: %d\n", m.NumGC)
fmt.Printf("GC 暂停: %v\n", time.Duration(m.PauseTotalNs))
fmt.Printf("上次 GC: %v\n", time.Unix(0, int64(m.LastGC)))
fmt.Printf("GC 频率: %.2f/s\n", float64(m.NumGC)/time.Since(time.Unix(0, int64(m.LastGC))).Seconds())
time.Sleep(5 * time.Second)
}
}
func main() {
go monitorGC()
// 模拟内存分配
for i := 0; i < 1000; i++ {
data := make([]byte, 1024*1024) // 1MB
_ = data
if i%100 == 0 {
runtime.GC() // 手动触发 GC
}
time.Sleep(100 * time.Millisecond)
}
}
2. GC 压力测试
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func gcPressureTest() {
var wg sync.WaitGroup
// 创建大量 goroutine
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// 频繁分配内存
for j := 0; j < 1000; j++ {
data := make([]byte, 1024) // 1KB
_ = data
if j%100 == 0 {
runtime.Gosched() // 让出 CPU
}
}
}(i)
}
wg.Wait()
}
func main() {
// 设置 GC 参数
runtime.GOMAXPROCS(4)
// 记录开始时间
start := time.Now()
// 运行压力测试
gcPressureTest()
// 记录结束时间
duration := time.Since(start)
// 输出统计信息
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("测试完成,耗时: %v\n", duration)
fmt.Printf("GC 次数: %d\n", m.NumGC)
fmt.Printf("GC 暂停总时间: %v\n", time.Duration(m.PauseTotalNs))
fmt.Printf("平均 GC 暂停: %v\n", time.Duration(m.PauseTotalNs)/time.Duration(m.NumGC))
}
3. GC 调优工具
package main
import (
"fmt"
"runtime"
"time"
)
type GCTuner struct {
targetPause time.Duration
currentPause time.Duration
gcPercent int
heapSize uint64
lastGC time.Time
}
func NewGCTuner() *GCTuner {
return &GCTuner{
targetPause: 10 * time.Millisecond,
gcPercent: 100,
}
}
func (t *GCTuner) Tune() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
// 计算当前 GC 暂停时间
if m.NumGC > 0 {
t.currentPause = time.Duration(m.PauseTotalNs) / time.Duration(m.NumGC)
}
// 调整 GC 参数
if t.currentPause > t.targetPause {
// GC 暂停时间过长,增加 GOGC
t.gcPercent += 10
if t.gcPercent > 200 {
t.gcPercent = 200
}
} else if t.currentPause < t.targetPause/2 {
// GC 暂停时间过短,减少 GOGC
t.gcPercent -= 10
if t.gcPercent < 50 {
t.gcPercent = 50
}
}
// 应用调整
runtime.GC()
fmt.Printf("GC 调优: 暂停时间=%v, 目标=%v, GOGC=%d\n",
t.currentPause, t.targetPause, t.gcPercent)
}
func main() {
tuner := NewGCTuner()
// 定期调优
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
tuner.Tune()
}
}
}
GC 性能分析
性能指标
- GC 频率:单位时间内的 GC 次数
- GC 暂停时间:单次 GC 的暂停时间
- GC 吞吐量:GC 处理的内存量
- 内存利用率:堆内存的使用效率
性能测试
package main
import (
"fmt"
"runtime"
"time"
)
func benchmarkGC() {
var m runtime.MemStats
// 记录开始状态
runtime.ReadMemStats(&m)
startGC := m.NumGC
startPause := m.PauseTotalNs
// 运行测试
start := time.Now()
for i := 0; i < 1000000; i++ {
data := make([]byte, 1024)
_ = data
}
duration := time.Since(start)
// 记录结束状态
runtime.ReadMemStats(&m)
endGC := m.NumGC
endPause := m.PauseTotalNs
// 计算性能指标
gcCount := endGC - startGC
totalPause := time.Duration(endPause - startPause)
avgPause := totalPause / time.Duration(gcCount)
fmt.Printf("测试结果:\n")
fmt.Printf(" 运行时间: %v\n", duration)
fmt.Printf(" GC 次数: %d\n", gcCount)
fmt.Printf(" GC 总暂停: %v\n", totalPause)
fmt.Printf(" 平均暂停: %v\n", avgPause)
fmt.Printf(" GC 频率: %.2f/s\n", float64(gcCount)/duration.Seconds())
}
func main() {
benchmarkGC()
}
面试题库
基础问题
什么是三色标记算法?
- 白色:未访问对象,候选垃圾
- 灰色:已发现但未扫描
- 黑色:已扫描完毕,存活对象
写屏障的作用是什么?
- 保证并发 GC 的正确性
- 标记新引用的对象
- 维护"黑不指白"不变式
Go GC 的触发条件?
- 堆大小达到目标
- 时间触发
- 手动触发
进阶问题
为什么 Go 的 GC 延迟低?
- 并发标记和清扫
- 增量清扫
- 写屏障优化
- 辅助标记机制
如何优化 GC 性能?
- 调整 GOGC 参数
- 减少内存分配
- 使用对象池
- 避免内存泄漏
GC 的 STW 时间如何优化?
- 减少根对象扫描时间
- 优化写屏障
- 使用并发标记
- 增量清扫
源码问题
gcMark 函数的主要流程?
- 标记根对象
- 并发标记
- 完成标记
写屏障的实现原理?
- 检查指针修改
- 标记新引用对象
- 维护标记状态
扩展阅读
相关章节
- 01-GMP调度模型深度解析 - GC 与调度的协作
- 03-内存模型与GC机制 - 内存分配与 GC 的关系
- 08-性能优化实战 - GC 性能调优
下一章预告:我们将深入 Go 的并发模型和锁机制,了解 Mutex、RWMutex、Atomic 等同步原语的实现原理。