推荐阅读:Go Garbage Collector
Golang 垃圾回收器概述
Go 语言的垃圾回收器(Garbage Collector, GC)是一个并发、三色标记、非分代的垃圾回收器,能够在程序运行的同时进行垃圾回收,最小化对程序性能的影响。
设计目标
- 低延迟: 减少 GC 导致的程序停顿时间(STW - Stop The World)
- 高吞吐: 在保证低延迟的同时,提高整体性能
- 并发执行: GC 与程序并发运行,减少对业务的影响
- 自动管理: 无需手动管理内存,减少内存泄漏风险
核心特性
- 并发标记: GC 标记阶段与程序并发执行
- 三色标记算法: 使用三色标记法追踪可达对象
- 写屏障(Write Barrier): 保证并发标记的正确性
- 增量清扫: 将清扫工作分散到多个 GC 周期
- 非分代: 不区分新生代和老生代
GC 核心算法
三色标记算法
三色标记算法是 Go GC 的核心算法,它将对象标记为三种颜色:
graph TB
subgraph "三色标记"
A[白色 White
未被访问的对象
可能是垃圾]
B[灰色 Gray
已被访问
引用的对象未扫描]
C[黑色 Black
已被访问
引用的对象已扫描]
end
A -->|开始扫描| B
B -->|扫描完成| C
style A fill:#ffffff,stroke:#000000
style B fill:#cccccc,stroke:#000000
style C fill:#000000,stroke:#ffffff,color:#ffffff
颜色说明:
- 白色(White):未被访问的对象(可能是垃圾)
- 灰色(Gray):已被访问,但其引用的对象还未被扫描
- 黑色(Black):已被访问,且其引用的对象都已被扫描
标记过程
stateDiagram-v2
[*] --> 初始状态: 所有对象都是白色
初始状态 --> 标记根对象: 扫描根对象
标记根对象 --> 根对象灰色: 根对象标记为灰色
根对象灰色 --> 扫描灰色对象: 从队列取出灰色对象
扫描灰色对象 --> 对象变黑: 将灰色对象标记为黑色
对象变黑 --> 引用对象变灰: 将其引用的白色对象标记为灰色
引用对象变灰 --> 检查队列: 检查是否还有灰色对象
检查队列 --> 扫描灰色对象: 有灰色对象
检查队列 --> 最终状态: 没有灰色对象
最终状态 --> [*]: 黑色=存活
白色=垃圾
详细步骤:
- 初始状态:所有对象都是白色
- 标记根对象:根对象(全局变量、栈变量等)标记为灰色
- 扫描灰色对象:
- 将灰色对象标记为黑色
- 将其引用的白色对象标记为灰色
- 重复直到没有灰色对象
- 最终状态:
- 黑色对象 = 存活对象
- 白色对象 = 垃圾对象(可回收)
标记过程示例图
graph TD
subgraph "步骤4: 最终状态"
D1[对象A
黑色]
D2[对象B
黑色]
D3[对象C
黑色]
Root4[根对象]
Root4 --> D1
D1 --> D2
D1 --> D3
end
subgraph "步骤3: 扫描A"
C1[对象A
黑色]
C2[对象B
灰色]
C3[对象C
灰色]
Root3[根对象]
Root3 --> C1
C1 --> C2
C1 --> C3
end
subgraph "步骤2: 标记根对象"
B1[对象A
灰色]
B2[对象B
白色]
B3[对象C
白色]
Root2[根对象]
Root2 --> B1
B1 --> B2
B1 --> B3
end
subgraph "步骤1: 初始状态"
A1[对象A
白色]
A2[对象B
白色]
A3[对象C
白色]
Root1[根对象]
Root1 --> A1
A1 --> A2
A1 --> A3
end
style A1 fill:#ffffff,stroke:#000000
style A2 fill:#ffffff,stroke:#000000
style A3 fill:#ffffff,stroke:#000000
style B1 fill:#cccccc,stroke:#000000
style C1 fill:#000000,stroke:#ffffff,color:#ffffff
style C2 fill:#cccccc,stroke:#000000
style C3 fill:#cccccc,stroke:#000000
style D1 fill:#000000,stroke:#ffffff,color:#ffffff
style D2 fill:#000000,stroke:#ffffff,color:#ffffff
style D3 fill:#000000,stroke:#ffffff,color:#ffffff
三色标记完整示例
graph TD
subgraph "最终状态:所有对象都是黑色"
R5[Root] --> A5[对象A
黑色]
A5 --> B5[对象B
黑色]
B5 --> C5[对象C
黑色]
A5 --> D5[对象D
黑色]
style A5 fill:#000000,stroke:#ffffff,color:#ffffff
style B5 fill:#000000,stroke:#ffffff,color:#ffffff
style C5 fill:#000000,stroke:#ffffff,color:#ffffff
style D5 fill:#000000,stroke:#ffffff,color:#ffffff
end
subgraph "扫描B:B变黑,引用的对象变灰"
R4[Root] --> A4[对象A
黑色]
A4 --> B4[对象B
黑色]
B4 --> C4[对象C
灰色]
A4 --> D4[对象D
灰色]
style A4 fill:#000000,stroke:#ffffff,color:#ffffff
style B4 fill:#000000,stroke:#ffffff,color:#ffffff
style C4 fill:#cccccc,stroke:#000000
style D4 fill:#cccccc,stroke:#000000
end
subgraph "扫描A:A变黑,引用的对象变灰"
R3[Root] --> A3[对象A
黑色]
A3 --> B3[对象B
灰色]
B3 --> C3[对象C
白色]
A3 --> D3[对象D
灰色]
style A3 fill:#000000,stroke:#ffffff,color:#ffffff
style B3 fill:#cccccc,stroke:#000000
style C3 fill:#ffffff,stroke:#000000
style D3 fill:#cccccc,stroke:#000000
end
subgraph "标记根对象:Root 引用的对象变灰"
R2[Root] --> A2[对象A
灰色]
A2 --> B2[对象B
白色]
B2 --> C2[对象C
白色]
A2 --> D2[对象D
白色]
style A2 fill:#cccccc,stroke:#000000
style B2 fill:#ffffff,stroke:#000000
style C2 fill:#ffffff,stroke:#000000
style D2 fill:#ffffff,stroke:#000000
end
subgraph "初始状态:所有对象都是白色"
R1[Root] --> A1[对象A
白色]
A1 --> B1[对象B
白色]
B1 --> C1[对象C
白色]
A1 --> D1[对象D
白色]
style A1 fill:#ffffff,stroke:#000000
style B1 fill:#ffffff,stroke:#000000
style C1 fill:#ffffff,stroke:#000000
style D1 fill:#ffffff,stroke:#000000
end
结果:
- 黑色对象(A, B, C, D):存活,需要保留
- 白色对象:不存在,说明没有垃圾
三色标记核心代码实现
三色在 Go 中通过位图表示:白色=未标记,灰色=在标记队列中,黑色=已扫描且标记。
1 | // 颜色通过 gcmarkBits 和标记队列状态表示: |
三色不变性
三色不变性(Tricolor Invariant)是保证并发标记正确性的核心约束。在并发标记过程中,程序可能修改对象引用关系,三色不变性确保不会丢失对存活对象的标记。
强三色不变性(Strong Tri-color Invariant)
定义:不允许黑色对象直接引用白色对象。
规则:
- 如果黑色对象要引用白色对象,必须先将白色对象标记为灰色
- 这确保了所有从根对象可达的白色对象都能被扫描到
可视化表示:
graph TD
subgraph "违反强三色不变性(不允许)"
R1[Root] --> A1[对象A
黑色]
A1 -->|直接引用| C1[对象C
白色
❌ 违反规则]
style R1 fill:#ffcccc
style A1 fill:#000000,stroke:#ffffff,color:#ffffff
style C1 fill:#ffffff,stroke:#ff0000,stroke-width:3px
end
subgraph "满足强三色不变性(允许)"
R2[Root] --> A2[对象A
黑色]
A2 -->|引用前先标记| C2[对象C
灰色
✅ 符合规则]
style R2 fill:#ffcccc
style A2 fill:#000000,stroke:#ffffff,color:#ffffff
style C2 fill:#cccccc,stroke:#00ff00,stroke-width:3px
end
实现方式:使用插入写屏障(Insert Write Barrier)
1 | // 伪代码:插入写屏障 |
示例场景:
sequenceDiagram
participant GC as GC标记器
participant A as 对象A黑色
participant C as 对象C白色
participant 程序 as 程序执行
participant 写屏障 as 写屏障
Note over GC,程序: 时刻 T1: GC 扫描完成
GC->>A: 标记为黑色
Note over C: C是白色
Note over GC,程序: 时刻 T2: 程序修改引用
程序->>A: A.next = C
程序->>写屏障: 检测到黑色引用白色
写屏障->>C: 将C标记为灰色
Note over C: ✅ 满足强三色不变性
Note over GC,程序: 时刻 T3: GC 继续扫描
GC->>C: 扫描灰色对象C
GC->>C: 标记为黑色
Note over C: ✅ C被正确标记
强三色不变性的保证:
- 黑色对象不会直接引用白色对象
- 所有可达的白色对象都会被标记为灰色
- 最终所有存活对象都会被标记为黑色
弱三色不变性(Weak Tri-color Invariant)
定义:允许黑色对象引用白色对象,但必须保证从根对象到白色对象的所有路径上,至少有一个灰色对象。
规则:
- 黑色对象可以引用白色对象
- 但必须存在一条路径:Root → … → 灰色对象 → … → 白色对象
- 这样即使黑色对象引用了白色对象,也能通过灰色对象最终扫描到白色对象
可视化表示:
flowchart TD
subgraph "违反弱三色不变性"
R2[Root] --> B2[黑色对象]
B2 -->|直接引用| W2[白色对象]
%% Note over R2,W2: Root到W2路径上没有灰色对象,W2可能被遗漏
style W2 fill:#ffffff,stroke:#ff0000,stroke-width:3px
end
subgraph "满足弱三色不变性"
R[Root] --> G[灰色对象]
G --> W[白色对象]
B[黑色对象] -->|允许引用| W
%% Note over R,W: Root到W路径上有灰色对象,W会被扫描到
style G fill:#cccccc,stroke:#00ff00,stroke-width:2px
style W fill:#ffffff,stroke:#000000
end
实现方式:使用删除写屏障(Delete Write Barrier)
1 | // 伪代码:删除写屏障 |
示例场景:
sequenceDiagram
participant Root as 根对象
participant G as 灰色对象
participant B as 黑色对象
participant W as 白色对象
Note over Root, W: 场景 1:满足弱三色不变性
Root->>G: 引用
G->>W: 引用
B->>W: 可以引用
Note over Root,W: 路径存在灰色对象\n✅ 白色对象会被扫描到
Note over Root, W: 场景 2:违反弱三色不变性
Root->>B: 引用
B->>W: 直接引用
Note over Root,W: 路径上没有灰色对象\n❌ 白色对象可能丢失
弱三色不变性的保证:
- 允许黑色对象引用白色对象
- 但保证存在灰色对象路径,确保白色对象最终被扫描
- 通过删除写屏障保护被删除引用的对象
强三色不变性 vs 弱三色不变性
| 特性 | 强三色不变性 | 弱三色不变性 |
|---|---|---|
| 约束 | 不允许黑色对象直接引用白色对象 | 允许黑色对象引用白色对象 |
| 路径要求 | 无特殊要求 | 路径上必须有灰色对象 |
| 实现方式 | 插入写屏障 | 删除写屏障 |
| 写屏障操作 | 标记新引用的白色对象为灰色 | 标记被删除引用的白色对象为灰色 |
| 性能影响 | 可能标记更多对象 | 可能标记更少对象 |
| 适用场景 | 插入操作频繁 | 删除操作频繁 |
Go 的混合写屏障(Hybrid Write Barrier)
Go 1.8+ 使用混合写屏障,结合了插入写屏障和删除写屏障的优点:
1 | // 伪代码:混合写屏障 |
混合写屏障的运行时实现(简化)
Go 运行时在指针写操作前插入写屏障,保证“当前栈未扫描完成前,新写的指针目标会被标记”。
1 | // goWriteBarrier 是编译器在 *dst = src 前调用的函数 |
混合写屏障的优势:
graph TB
A[混合写屏障] --> B[插入写屏障
保护新引用]
A --> C[删除写屏障
保护被删除引用]
B --> D[满足强三色不变性]
C --> E[满足弱三色不变性]
D --> F[保证并发标记正确性]
E --> F
style A fill:#ffcccc
style B fill:#ccffcc
style C fill:#ccccff
style F fill:#ffffcc
特点:
- 同时保证强三色和弱三色不变性
- 更全面的保护,减少丢失标记的风险
- 适合 Go 的并发标记场景
三色不变性的重要性
flowchart TD
A[三色不变性] --> B[保证并发标记正确性]
B --> C[不会丢失存活对象]
C --> D[不会误回收存活对象]
A --> E[通过写屏障实现]
E --> F[插入写屏障
强三色不变性]
E --> G[删除写屏障
弱三色不变性]
E --> H[混合写屏障
Go 1.8+]
style A fill:#ffcccc
style C fill:#ccffcc
style D fill:#ccffcc
style H fill:#ffffcc
总结:
- 强三色不变性:不允许黑色对象直接引用白色对象,通过插入写屏障实现
- 弱三色不变性:允许黑色对象引用白色对象,但路径上必须有灰色对象,通过删除写屏障实现
- 混合写屏障:Go 使用混合写屏障,同时保证两种不变性,提供更全面的保护
并发标记的挑战
在并发标记过程中,程序可能修改对象之间的引用关系,导致标记错误:
问题场景:丢失标记
sequenceDiagram
participant GC as GC标记器
participant A as 对象A
participant B as 对象B
participant C as 对象C
participant 程序 as 程序执行
Note over GC,程序: 时刻 T1: GC 扫描完成
GC->>A: 标记为黑色
GC->>B: 标记为黑色
Note over C: C是白色(垃圾)
Note over GC,程序: 时刻 T2: 程序修改引用
程序->>A: A.next = C
程序->>B: B.next = nil
Note over C: C现在被A引用
但仍然是白色
Note over GC,程序: 时刻 T3: GC 完成标记
GC->>C: C仍然是白色
Note over C: ❌ C被误判为垃圾!
问题:C 对象实际上被 A 引用了,但因为标记时是白色,被误判为垃圾。
丢失标记的可视化
graph TD
subgraph "时刻 T3: GC 完成(错误)"
R3[Root] --> A3[对象A
黑色]
A3 -->|引用| C3[对象C
白色
❌ 误判为垃圾]
A3 -.-> B3[对象B
黑色]
style A3 fill:#000000,stroke:#ffffff,color:#ffffff
style B3 fill:#000000,stroke:#ffffff,color:#ffffff
style C3 fill:#ffffff,stroke:#ff0000,stroke-width:3px
end
subgraph "时刻 T2: 程序修改引用"
R2[Root] --> A2[对象A
黑色]
A2 -->|A.next = C| C2[对象C
白色]
A2 -.->|B.next = nil| B2[对象B
黑色]
style A2 fill:#000000,stroke:#ffffff,color:#ffffff
style B2 fill:#000000,stroke:#ffffff,color:#ffffff
style C2 fill:#ffffff,stroke:#ff0000,stroke-width:3px
end
subgraph "时刻 T1: GC 扫描完成"
R1[Root] --> A1[对象A
黑色]
A1 --> B1[对象B
黑色]
A1 -.->|无引用| C1[对象C
白色
垃圾]
style A1 fill:#000000,stroke:#ffffff,color:#ffffff
style B1 fill:#000000,stroke:#ffffff,color:#ffffff
style C1 fill:#ffffff,stroke:#ff0000,stroke-width:3px
end
解决方案:写屏障(Write Barrier)
写屏障在对象引用关系改变时,将被引用的对象标记为灰色,确保不会丢失标记。
写屏障规则:如果黑色对象引用白色对象,将白色对象标记为灰色
sequenceDiagram
participant GC as GC标记器
participant A as 对象A黑色
participant B as 对象B黑色
participant C as 对象C白色
participant 程序 as 程序执行
participant 写屏障 as 写屏障
Note over GC,程序: "时刻 T1: GC 扫描完成"
GC->>A: 标记为黑色
GC->>B: 标记为黑色
Note over C: C是白色
Note over GC,程序: "时刻 T2: 程序修改引用"
程序->>A: A.next = C
程序->>写屏障: 检测到黑色引用白色
写屏障->>C: 将C标记为灰色
Note over C: ✅ C被保护
Note over GC,程序: "时刻 T3: GC 继续扫描"
GC->>C: 扫描灰色对象C
GC->>C: 标记为黑色
Note over C: ✅ C被正确标记
Note over GC,程序: "时刻 T4: GC 完成"
Note over A,C: 所有对象正确标记
写屏障保护过程
graph TD
subgraph "时刻 T4: GC 完成(正确)"
R4[Root] --> A4[对象A
黑色]
A4 --> C4[对象C
黑色
✅ 正确保留]
A4 -.-> B4[对象B
黑色]
style A4 fill:#000000,stroke:#ffffff,color:#ffffff
style B4 fill:#000000,stroke:#ffffff,color:#ffffff
style C4 fill:#000000,stroke:#00ff00,stroke-width:3px,color:#ffffff
end
subgraph "时刻 T3: GC 继续扫描"
R3[Root] --> A3[对象A
黑色]
A3 --> C3[对象C
灰色]
A3 -.-> B3[对象B
黑色]
style A3 fill:#000000,stroke:#ffffff,color:#ffffff
style B3 fill:#000000,stroke:#ffffff,color:#ffffff
style C3 fill:#cccccc,stroke:#000000
end
subgraph "时刻 T2: 写屏障触发"
R2[Root] --> A2[对象A
黑色]
A2 -->|A.next = C| C2[对象C
灰色
✅ 写屏障保护]
A2 -.-> B2[对象B
黑色]
style A2 fill:#000000,stroke:#ffffff,color:#ffffff
style B2 fill:#000000,stroke:#ffffff,color:#ffffff
style C2 fill:#cccccc,stroke:#00ff00,stroke-width:3px
end
subgraph "时刻 T1: GC 扫描完成"
R1[Root] --> A1[对象A
黑色]
A1 --> B1[对象B
黑色]
A1 -.->|无引用| C1[对象C
白色]
style A1 fill:#000000,stroke:#ffffff,color:#ffffff
style B1 fill:#000000,stroke:#ffffff,color:#ffffff
style C1 fill:#ffffff,stroke:#000000
end
GC 核心组件
1. GC 工作器(GC Worker)
GC 工作器是执行标记和清扫任务的 goroutine。
结构定义(简化)
1 | type gcWork struct { |
GC 工作器的作用
- 标记对象: 扫描灰色对象,标记其引用的对象
- 并行工作: 多个工作器并行标记,提高效率
- 工作窃取: 工作器可以窃取其他工作器的工作
gcWork 与 workbuf 核心实现
每个 P 有一个 gcWork,内含两个 workbuf,用于存放灰色对象指针;队列满时与全局 work 交换。
1 | // workbuf 是标记队列的一个缓冲区 |
2. 标记队列(Mark Queue)
标记队列用于存储待扫描的灰色对象。
标记队列结构
graph LR
subgraph "标记队列"
A[对象A
灰色] --> B[对象B
灰色]
B --> C[对象C
灰色]
C --> D[对象D
灰色]
end
E[GC工作器1] -->|取出| A
F[GC工作器2] -->|取出| B
G[GC工作器3] -->|取出| C
A -->|扫描| H[发现新对象]
H -->|入队| I[标记队列]
style A fill:#cccccc,stroke:#000000
style B fill:#cccccc,stroke:#000000
style C fill:#cccccc,stroke:#000000
style D fill:#cccccc,stroke:#000000
style E fill:#ffcccc
style F fill:#ccffcc
style G fill:#ccccff
工作流程
flowchart TD
A[根对象] -->|1. 入队| B[标记队列
灰色对象]
B -->|2. 工作器取出| C[对象变黑]
C -->|3. 扫描引用| D[发现新对象]
D -->|4. 新对象变灰| E[入队]
E -->|5. 检查队列| F{队列为空?}
F -->|否| B
F -->|是| G[标记完成]
style A fill:#ffcccc
style B fill:#cccccc,stroke:#000000
style C fill:#000000,stroke:#ffffff,color:#ffffff
style G fill:#ccffcc
3. 写屏障(Write Barrier)
写屏障在对象引用改变时触发,确保并发标记的正确性。
插入写屏障(Insert Write Barrier)
1 | // 伪代码 |
写屏障类型
Go 1.8+ 使用混合写屏障(Hybrid Write Barrier),结合了插入写屏障和删除写屏障的优点。
4. 根对象扫描器(Root Scanner)
根对象是 GC 的起点,包括:
graph TB
A[根对象] --> B[全局变量]
A --> C[栈变量
所有goroutine的栈]
A --> D[寄存器变量]
A --> E[运行时数据结构]
B --> F[标记为灰色]
C --> F
D --> F
E --> F
style A fill:#ffcccc
style F fill:#cccccc,stroke:#000000
根扫描流程
flowchart TD
A[开始根扫描] --> B[扫描全局变量]
B --> C[标记为灰色]
C --> D[扫描所有goroutine栈]
D --> E[标记栈变量为灰色]
E --> F[扫描寄存器]
F --> G[标记寄存器变量为灰色]
G --> H[扫描运行时数据]
H --> I[标记为灰色]
I --> J[根扫描完成]
style A fill:#ffcccc
style C fill:#cccccc,stroke:#000000
style E fill:#cccccc,stroke:#000000
style G fill:#cccccc,stroke:#000000
style I fill:#cccccc,stroke:#000000
style J fill:#ccffcc
根扫描与 GC 工作器代码实现
1 | // markRoots 在 GC 开始时扫描所有根对象并加入标记队列 |
5. 清扫器(Sweeper)
清扫器负责回收标记为白色的对象(垃圾)。
清扫流程
flowchart TD
A[开始清扫] --> B[扫描所有span]
B --> C{对象颜色?}
C -->|白色| D[标记为空闲]
C -->|黑色| E[保留对象]
D --> F{span完全空闲?}
F -->|是| G[归还给mheap]
F -->|否| H[放回mcentral]
E --> I[继续扫描]
G --> I
H --> I
I --> J{还有span?}
J -->|是| B
J -->|否| K[清扫完成]
style A fill:#ffcccc
style D fill:#ffffff,stroke:#ff0000
style E fill:#000000,stroke:#ffffff,color:#ffffff
style K fill:#ccffcc
增量清扫
Go 使用增量清扫,将清扫工作分散到多个 GC 周期:
gantt
title 增量清扫时间线
dateFormat X
axisFormat %s
section GC周期1
清扫部分内存 :0, 30
section GC周期2
继续清扫剩余内存 : 30000, 60
section GC周期3
完成清扫 :60000, 90
优势:
- 避免一次性清扫导致的长停顿
- 将清扫工作分散到多个周期
- 与程序并发执行,减少影响
清扫器核心代码实现
1 | // sweep 清扫一个 span:回收白色对象,更新 allocBits |
GC 执行流程
完整的 GC 周期
stateDiagram-v2
[*] --> GC开始: 触发GC
GC开始 --> 准备阶段: STW
准备阶段: 停止所有goroutine
扫描栈
启用写屏障
准备标记队列
准备阶段 --> 标记阶段: 启动GC工作器
标记阶段: 并发标记对象
写屏障保护
程序继续运行
标记阶段 --> 标记终止: STW
标记终止: 停止所有goroutine
完成最后标记
关闭写屏障
标记终止 --> 清扫阶段: 恢复运行
清扫阶段: 并发清扫垃圾
归还空闲内存
增量执行
清扫阶段 --> GC结束: 清扫完成
GC结束 --> [*]
note right of 准备阶段
STW时间
通常 < 1ms
end note
note right of 标记阶段
并发执行
GC与程序同时运行
end note
note right of 标记终止
STW时间
通常 < 1ms
end note
note right of 清扫阶段
增量执行
分散到多个周期
end note
GC 周期时间线
gantt
title GC周期时间线
dateFormat X
axisFormat %S
section STW1阶段
准备阶段 :0, 1
section 并发标记阶段
标记阶段 :1000, 30
section STW2阶段
标记终止 :30000, 31
section 并发清理阶段
清扫阶段 :31000, 50
详细流程说明
阶段 1: 准备阶段(STW - Stop The World)
1 | // gcStart 启动新一轮 GC |
STW 时间: 通常 < 1ms
阶段 2: 标记阶段(并发)
1 | // gcBgMarkStartWorkers 启动后台标记 worker |
并发执行: GC 与程序同时运行
阶段 3: 标记终止(STW)
1 | // gcMarkDone 判断标记是否完成,若完成则进入标记终止 |
STW 时间: 通常 < 1ms
阶段 4: 清扫阶段(并发)
1 | // gcSweepTerminate 标记终止时调用,初始化清扫状态 |
增量执行: 清扫工作分散到多个周期
GC 触发条件
1. 自动触发
GC 会在以下情况自动触发:
堆内存增长触发
graph LR
A[上次GC后的堆内存] -->|增长| B{当前堆内存}
B -->|计算公式| C["当前堆内存 =
上次堆内存 × (1 + GOGC/100)"]
C -->|达到阈值| D[触发GC]
E[GOGC=100示例] --> F["增长100%时触发
当前 = 上次 × 2"]
E --> G["GOGC=50示例
增长50%时触发
当前 = 上次 × 1.5"]
E --> H["GOGC=200示例
增长200%时触发
当前 = 上次 × 3"]
style D fill:#ffcccc
style F fill:#ccffcc
计算公式:
1 | 当前堆内存 = 上次 GC 后的堆内存 × (1 + GOGC/100) |
示例:
GOGC = 100:当前堆内存 = 上次堆内存 × 2(增长 100% 时触发)GOGC = 50:当前堆内存 = 上次堆内存 × 1.5(增长 50% 时触发)GOGC = 200:当前堆内存 = 上次堆内存 × 3(增长 200% 时触发)
GC 触发条件代码实现
1 | // gcTrigger 表示一次 GC 的触发条件 |
代码触发
1 | // 手动触发 GC |
2. 强制触发
内存限制触发
1 | // Go 1.19+ 支持内存限制 |
系统压力触发
当系统内存压力大时,Go 会主动触发更频繁的 GC。
3. 定时触发
Go 运行时可能会定时触发 GC,确保内存及时回收。
GC 性能指标
关键指标
1. GC 频率(GC Frequency)
1 | GC 频率 = GC 次数 / 运行时间 |
2. GC 暂停时间(GC Pause Time)
1 | STW 时间 = 准备阶段时间 + 标记终止时间 |
3. GC CPU 占用(GC CPU Usage)
1 | GC CPU 占用 = GC 使用的 CPU 时间 / 总 CPU 时间 |
4. 堆内存使用(Heap Usage)
1 | 堆内存使用 = 当前堆内存 / 最大堆内存 |
查看 GC 统计信息
方式 1: 使用 GODEBUG
1 | # 设置环境变量 |
输出示例:
1 | gc 1 @0.001s 2%: 0.010+0.20+0.002 ms clock, 0.040+0.20/0.10/0.20+0.008 ms cpu, 4->4->0 MB, 5 MB goal, 4 P |
字段说明:
gc 1: GC 周期编号@0.001s: 程序运行时间2%: GC 占用的 CPU 百分比0.010+0.20+0.002 ms: STW 时间(准备+标记终止+其他)4->4->0 MB: 堆内存变化(GC前->GC后->存活对象)5 MB goal: GC 目标堆大小4 P: 使用的 P 数量
方式 2: 使用 runtime.ReadMemStats
1 | package main |
方式 3: 使用 pprof
1 | # 获取 GC 统计 |
GC 调优参数
1. GOGC 参数
控制 GC 的触发频率:
1 | # 默认值:100 |
GOGC 的影响
graph TB
subgraph "GOGC = 50"
A1[GC更频繁] --> A2[内存使用更少]
A1 --> A3[CPU占用可能更高]
A2 --> A4[适合内存受限环境]
end
subgraph "GOGC = 100 默认"
B1[平衡内存和CPU] --> B2[适合大多数场景]
end
subgraph "GOGC = 200"
C1[GC更少] --> C2[内存使用更多]
C1 --> C3[CPU占用可能更低]
C2 --> C4[适合内存充足环境]
end
style A4 fill:#ffcccc
style B2 fill:#ccffcc
style C4 fill:#ccccff
对比表:
| GOGC 值 | GC 频率 | 内存使用 | CPU 占用 | 适用场景 |
|---|---|---|---|---|
| 50 | 更频繁 | 更少 | 可能更高 | 内存受限环境 |
| 100(默认) | 平衡 | 平衡 | 平衡 | 大多数场景 |
| 200 | 更少 | 更多 | 可能更低 | 内存充足环境 |
2. GOMEMLIMIT 参数
设置内存使用上限(Go 1.19+):
1 | # 设置最大内存使用为 2GB |
GOMEMLIMIT 的影响
flowchart TD
A[设置 GOMEMLIMIT] --> B[监控内存使用]
B --> C{内存超过限制?}
C -->|是| D[强制触发GC]
C -->|否| E[正常执行]
D --> F[可能导致更频繁的GC]
F --> G[适合容器环境]
E --> B
style A fill:#ffcccc
style D fill:#ff9999
style G fill:#ccffcc
特点:
- 防止程序使用过多内存
- 超过限制时强制触发 GC
- 可能导致更频繁的 GC
- 适合容器环境
3. 其他环境变量
1 | # GC 调试信息 |
GC 优化策略
1. 减少内存分配
使用对象池
1 | package main |
预分配切片容量
1 | // 不好:频繁扩容,增加 GC 压力 |
2. 减少指针使用
使用值类型
1 | // 小结构体使用值类型 |
使用数组而非切片
1 | // 固定大小使用数组 |
3. 优化数据结构
减少嵌套指针
1 | // 不好:深层指针嵌套 |
使用结构体数组
1 | // 不好:指针数组 |
4. 合理设置 GC 参数
根据场景调整 GOGC
1 | // 内存受限环境 |
5. 监控 GC 性能
定期检查 GC 统计
1 | package main |
GC 与内存分配器的协作
协作流程
flowchart TD
A[内存分配] --> B[mcache/mcentral/mheap]
B --> C[对象使用中]
C --> D[GC 标记阶段]
D --> E1[扫描 mcache 中的对象]
D --> E2[扫描 mcentral 中的对象]
D --> E3[扫描 mheap 中的对象]
E1 --> F[标记存活对象
黑色]
E2 --> F
E3 --> F
F --> G[GC 清扫阶段]
G --> H1[回收未标记对象
白色]
G --> H2[将空闲 span 归还 mheap]
G --> H3[更新 mcache/mcentral]
H1 --> I[内存可复用]
H2 --> I
H3 --> I
style A fill:#ffcccc
style D fill:#ccffcc
style F fill:#000000,stroke:#ffffff,color:#ffffff
style G fill:#ccccff
style I fill:#ffffcc
标记阶段与分配器的交互
sequenceDiagram
participant 程序 as 程序执行
participant mcache as mcache
participant 写屏障 as 写屏障
participant GC as GC标记器
Note over 程序,GC: 分配对象时
程序->>mcache: 从 mcache 分配对象
mcache-->>程序: 返回对象
程序->>写屏障: 如果被根对象引用
写屏障->>写屏障: 标记对象为灰色
Note over 程序,GC: GC 标记时
GC->>mcache: 扫描 mcache 中的对象
mcache-->>GC: 返回对象列表
GC->>GC: 标记可达对象为黑色
GC->>mcache: 更新对象的标记位
清扫阶段与分配器的交互
flowchart TD
A[GC 清扫开始] --> B[扫描所有 span]
B --> C{对象颜色?}
C -->|白色| D[回收对象]
C -->|黑色| E[保留对象]
D --> F{span状态?}
F -->|完全空闲| G[归还 mheap]
F -->|部分空闲| H[放回 mcentral]
G --> I[mcache 可以复用]
H --> I
E --> J[继续扫描]
I --> J
style A fill:#ffcccc
style D fill:#ffffff,stroke:#ff0000
style E fill:#000000,stroke:#ffffff,color:#ffffff
style I fill:#ccffcc
常见问题与解决方案
1. GC 暂停时间过长
问题表现
graph TB
A[GC暂停时间 > 10ms] --> B[程序响应变慢]
B --> C[用户体验差]
style A fill:#ffcccc
style C fill:#ff9999
解决方案
flowchart TD
A[GC暂停时间过长] --> B{解决方案}
B -->|方案1| C1[减少内存分配
对象池/预分配/减少指针]
B -->|方案2| C2[调整GOGC参数
GOGC=50更频繁GC]
B -->|方案3| C3[优化数据结构
减少指针嵌套/值类型]
C1 --> D[降低GC压力]
C2 --> D
C3 --> D
style A fill:#ffcccc
style D fill:#ccffcc
2. GC CPU 占用过高
问题表现
graph TB
A[GC CPU占用 > 50%] --> B[整体性能下降]
B --> C[CPU资源浪费]
style A fill:#ffcccc
style C fill:#ff9999
解决方案
flowchart TD
A[GC CPU占用过高] --> B{解决方案}
B -->|方案1| C1[减少内存分配
对象池/复用缓冲区]
B -->|方案2| C2[调整GOGC参数
GOGC=200更宽松]
B -->|方案3| C3[优化代码
减少不必要分配]
C1 --> D[降低GC频率]
C2 --> D
C3 --> D
style A fill:#ffcccc
style D fill:#ccffcc
3. 内存使用过多
问题表现
graph TB
A[堆内存持续增长] --> B[可能触发OOM]
B --> C[内存使用率低]
style A fill:#ffcccc
style C fill:#ff9999
解决方案
flowchart TD
A[内存使用过多] --> B{解决方案}
B -->|方案1| C1[调整GOGC参数
GOGC=50更积极]
B -->|方案2| C2[设置内存限制
GOMEMLIMIT]
B -->|方案3| C3[检查内存泄漏
pprof对比分析]
C1 --> D[降低内存使用]
C2 --> D
C3 --> D
style A fill:#ffcccc
style D fill:#ccffcc
4. GC 频率过高
问题表现
graph TB
A[GC频率 > 10次/秒] --> B[程序性能下降]
B --> C[CPU占用高]
style A fill:#ffcccc
style C fill:#ff9999
解决方案
flowchart TD
A[GC频率过高] --> B{解决方案}
B -->|方案1| C1[减少内存分配速度
对象池/批量处理]
B -->|方案2| C2[调整GOGC参数
GOGC=200允许更多增长]
B -->|方案3| C3[优化代码
复用对象]
C1 --> D[降低GC频率]
C2 --> D
C3 --> D
style A fill:#ffcccc
style D fill:#ccffcc
实际应用示例
示例 1: 监控 GC 性能
1 | package main |
示例 2: 优化内存分配
1 | package main |
示例 3: 调整 GC 参数
1 | package main |
总结
Go 垃圾回收器的核心特点:
- 并发执行: GC 与程序并发运行,减少停顿时间
- 三色标记: 使用三色标记算法追踪可达对象
- 写屏障保护: 确保并发标记的正确性
- 低延迟: STW 时间通常 < 1ms
- 自动管理: 无需手动管理内存
GC 流程:
flowchart LR
A[准备阶段
STW] --> B[标记阶段
并发]
B --> C[标记终止
STW]
C --> D[清扫阶段
并发]
style A fill:#ffcccc
style B fill:#ccffcc
style C fill:#ffcccc
style D fill:#ccffcc
核心组件:
graph TB
A[GC核心组件] --> B[GC工作器
执行标记和清扫任务]
A --> C[标记队列
存储待扫描的灰色对象]
A --> D[写屏障
保护并发标记的正确性]
A --> E[根扫描器
扫描根对象]
A --> F[清扫器
回收垃圾对象]
style A fill:#ffcccc
style B fill:#ccffcc
style C fill:#ccffcc
style D fill:#ccffcc
style E fill:#ccffcc
style F fill:#ccffcc
优化建议:
graph TD
A[GC优化建议] --> B[减少内存分配
对象池/预分配]
A --> C[减少指针使用
值类型/数组]
A --> D[合理设置GC参数
GOGC/GOMEMLIMIT]
A --> E[监控GC性能
GODEBUG/pprof]
A --> F[优化数据结构
减少嵌套/值类型]
style A fill:#ffcccc
style B fill:#ccffcc
style C fill:#ccffcc
style D fill:#ccffcc
style E fill:#ccffcc
style F fill:#ccffcc
理解 Go GC 的工作原理,有助于编写高性能的 Go 程序,减少 GC 压力,提高程序性能。