golang-垃圾回收器


推荐阅读:Go Garbage Collector

Golang 垃圾回收器概述

Go 语言的垃圾回收器(Garbage Collector, GC)是一个并发、三色标记、非分代的垃圾回收器,能够在程序运行的同时进行垃圾回收,最小化对程序性能的影响。

设计目标

  1. 低延迟: 减少 GC 导致的程序停顿时间(STW - Stop The World)
  2. 高吞吐: 在保证低延迟的同时,提高整体性能
  3. 并发执行: GC 与程序并发运行,减少对业务的影响
  4. 自动管理: 无需手动管理内存,减少内存泄漏风险

核心特性

  • 并发标记: 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
    [*] --> 初始状态: 所有对象都是白色
    初始状态 --> 标记根对象: 扫描根对象
    标记根对象 --> 根对象灰色: 根对象标记为灰色
    根对象灰色 --> 扫描灰色对象: 从队列取出灰色对象
    扫描灰色对象 --> 对象变黑: 将灰色对象标记为黑色
    对象变黑 --> 引用对象变灰: 将其引用的白色对象标记为灰色
    引用对象变灰 --> 检查队列: 检查是否还有灰色对象
    检查队列 --> 扫描灰色对象: 有灰色对象
    检查队列 --> 最终状态: 没有灰色对象
    最终状态 --> [*]: 黑色=存活
白色=垃圾

详细步骤

  1. 初始状态:所有对象都是白色
  2. 标记根对象:根对象(全局变量、栈变量等)标记为灰色
  3. 扫描灰色对象
    • 将灰色对象标记为黑色
    • 将其引用的白色对象标记为灰色
    • 重复直到没有灰色对象
  4. 最终状态
    • 黑色对象 = 存活对象
    • 白色对象 = 垃圾对象(可回收)

标记过程示例图

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
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
// 颜色通过 gcmarkBits 和标记队列状态表示:
// 白色:gcmarkBits 未设置
// 灰色:在 workbuf 队列中待扫描
// 黑色:gcmarkBits 已设置且已从队列中取出并扫描完成

// markBits 封装 span 的 GC 标记位图
type markBits struct {
bytep *uint8 // 指向位图字节
mask uint8 // 当前对象的位掩码
index uintptr // 当前对象在 span 中的索引
}

// isMarked 检查对象是否已被标记(黑色)
func (m *markBits) isMarked() bool {
return (*m.bytep & m.mask) != 0
}

// setMarked 将对象标记为黑色
func (m *markBits) setMarked() {
*m.bytep |= m.mask
}

// setMarkedNonAtomic 非原子方式设置标记(STW 时使用)
func (m *markBits) setMarkedNonAtomic() {
*m.bytep |= m.mask
}

// heapBitsForAddr 根据对象地址获取对应的 heapBits
func heapBitsForAddr(addr uintptr) heapBits {
// 通过 mheap.arena 找到 span,再计算位图位置
s := spanOf(addr)
if s == nil {
return heapBits{}
}
return s.heapBitsForIndex((addr - s.base()) / s.elemsize)
}

// markObject 将对象标记为灰色:加入标记队列供后续扫描
// 参数 obj 为指向堆对象的指针
func markObject(base, obj uintptr) {
if obj == 0 {
return
}
// 获取对象所在 span 和索引
s := spanOf(obj)
if s == nil {
return
}
objIndex := (obj - s.base()) / s.elemsize

// 若已标记则不再入队
if s.gcmarkBits.isMarked(objIndex) {
return
}

// 标记为灰色:先设置 gcmarkBits,再入队(避免重复扫描)
s.gcmarkBits.setMarked(objIndex)

// 将对象指针放入 GC 工作队列
gcw := getg().m.p.ptr().gcw
gcw.put(obj)
}

三色不变性

三色不变性(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
2
3
4
5
6
7
8
9
10
// 伪代码:插入写屏障
func writebarrierptr(dst *unsafe.Pointer, src unsafe.Pointer) {
// 如果 dst 是黑色对象,src 是白色对象
if isBlack(dst) && isWhite(src) {
// 将 src 标记为灰色(保护白色对象)
shade(src) // 满足强三色不变性
}
// 执行实际的写操作
*dst = src
}

示例场景

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
2
3
4
5
6
7
8
9
10
11
// 伪代码:删除写屏障
func writebarrierptr(dst *unsafe.Pointer, src unsafe.Pointer) {
old := *dst
// 如果删除的引用指向白色对象
if isWhite(old) {
// 将被删除的对象标记为灰色(保护白色对象)
shade(old) // 满足弱三色不变性
}
// 执行实际的写操作
*dst = src
}

示例场景

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 伪代码:混合写屏障
func hybridWriteBarrier(dst *unsafe.Pointer, src unsafe.Pointer) {
old := *dst

// 1. 插入写屏障:如果黑色对象引用白色对象
if isBlack(dst) && isWhite(src) {
shade(src) // 标记新引用的白色对象为灰色
}

// 2. 删除写屏障:如果删除的引用指向白色对象
if isWhite(old) {
shade(old) // 标记被删除引用的白色对象为灰色
}

// 执行实际的写操作
*dst = src
}

混合写屏障的运行时实现(简化)

Go 运行时在指针写操作前插入写屏障,保证“当前栈未扫描完成前,新写的指针目标会被标记”。

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
// goWriteBarrier 是编译器在 *dst = src 前调用的函数
// 注意:实际实现为汇编,这里用等价逻辑说明

// 写屏障只在 GC 的标记阶段启用
func gcWriteBarrier(dst *uintptr, src uintptr) {
if !writeBarrier.enabled {
*dst = src
return
}

// 若 src 为 nil 或非堆对象,无需屏障
if src == 0 {
*dst = 0
return
}

// 将被覆盖的旧值、新值都标记为“需扫描”
// 这样满足:强三色(新引用被保护)+ 弱三色(旧引用被保护)
if src != 0 {
shade(src) // 新引用:插入写屏障
}
old := *dst
if old != 0 {
shade(old) // 旧引用:删除写屏障
}

*dst = src
}

// shade 将指针对应的对象加入当前 P 的标记队列(变灰)
func shade(obj uintptr) {
if obj == 0 {
return
}
// 对象地址 -> span -> base
base := findObject(obj)
if base == 0 {
return
}
markObject(base, obj)
}

// findObject 找到对象所在 span 的基地址
func findObject(p uintptr) uintptr {
s := spanOf(p)
if s == nil {
return 0
}
return s.base()
}

混合写屏障的优势

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
2
3
4
5
6
7
8
type gcWork struct {
// 工作缓冲区
wbuf1, wbuf2 *workbuf

// 标记队列
bytesMarked uint64
scanWork int64
}

GC 工作器的作用

  1. 标记对象: 扫描灰色对象,标记其引用的对象
  2. 并行工作: 多个工作器并行标记,提高效率
  3. 工作窃取: 工作器可以窃取其他工作器的工作

gcWork 与 workbuf 核心实现

每个 P 有一个 gcWork,内含两个 workbuf,用于存放灰色对象指针;队列满时与全局 work 交换。

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
// workbuf 是标记队列的一个缓冲区
type workbuf struct {
workbufhdr
obj [(_WorkbufSize - unsafe.Sizeof(workbufhdr{})) / sys.PtrSize]uintptr
}

type workbufhdr struct {
node lfnode // 链表节点,用于全局队列
nobj uintptr // 当前 buf 中对象数量
}

const _WorkbufSize = 2048 // 每个 buf 约 2048 个指针

// gcWork 是每个 P 的本地标记工作队列
type gcWork struct {
wbuf1 *workbuf // 当前使用的 buf
wbuf2 *workbuf // 备用 buf,用于与全局交换
bytesMarked uint64
scanWork int64
}

// put 将灰色对象指针放入本地队列
func (w *gcWork) put(obj uintptr) {
wbuf := w.wbuf1
if wbuf == nil {
wbuf = getempty()
w.wbuf1 = wbuf
}
if wbuf.nobj >= len(wbuf.obj) {
// 当前 buf 满,与全局 full 队列交换
w.wbuf1, w.wbuf2 = w.wbuf2, w.wbuf1
wbuf = w.wbuf1
if wbuf == nil {
wbuf = getempty()
w.wbuf1 = wbuf
}
if wbuf.nobj >= len(wbuf.obj) {
putfull(wbuf)
wbuf = getempty()
w.wbuf1 = wbuf
}
}
wbuf.obj[wbuf.nobj] = obj
wbuf.nobj++
}

// tryGet 从本地队列取一个灰色对象;空则返回 0
func (w *gcWork) tryGet() uintptr {
wbuf := w.wbuf1
if wbuf == nil {
return 0
}
if wbuf.nobj == 0 {
w.wbuf1, w.wbuf2 = w.wbuf2, w.wbuf1
wbuf = w.wbuf1
if wbuf == nil || wbuf.nobj == 0 {
return 0
}
}
wbuf.nobj--
return wbuf.obj[wbuf.nobj]
}

// get 从本地或全局队列取灰色对象;空则阻塞直到有或标记结束
func (w *gcWork) get() uintptr {
for {
if obj := w.tryGet(); obj != 0 {
return obj
}
// 尝试从全局队列窃取
wbuf := trygetfull()
if wbuf == nil {
return 0 // 全局也无,由上层判断是否结束
}
w.wbuf1 = wbuf
}
}

// drain 排空本地队列,将未处理的对象交给全局
func (w *gcWork) drain() {
for wbuf := w.wbuf1; wbuf != nil; wbuf = w.wbuf1 {
w.wbuf1 = w.wbuf2
w.wbuf2 = nil
if wbuf.nobj > 0 {
putfull(wbuf)
} else {
putempty(wbuf)
}
}
w.wbuf1 = nil
w.wbuf2 = nil
}

// empty 判断本地队列是否为空
func (w *gcWork) empty() bool {
return w.wbuf1 == nil || w.wbuf1.nobj == 0
}

// getempty / putempty / putfull / trygetfull 为全局 work 池的获取/归还(此处省略实现)

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
2
3
4
5
6
7
8
9
10
// 伪代码
func writebarrierptr(dst *unsafe.Pointer, src unsafe.Pointer) {
// 如果 dst 是黑色对象,src 是白色对象
if isBlack(dst) && isWhite(src) {
// 将 src 标记为灰色
shade(src)
}
// 执行实际的写操作
*dst = src
}

写屏障类型

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
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
// markRoots 在 GC 开始时扫描所有根对象并加入标记队列
func markRoots() {
// 1. 扫描全局变量、bss 等
for _, datap := range activeModules() {
markrootBlock(datap.data, datap.edata-datap.data, datap.gcdatamask.bytedata)
markrootBlock(datap.bss, datap.ebss-datap.bss, datap.gcbssmask.bytedata)
}

// 2. 扫描所有 P 的 cache(mcache 中的 span)
for _, p := range allp {
if p.mcache != nil {
markrootSpans(p.mcache)
}
}

// 3. 扫描所有 goroutine 栈
for _, gp := range allgs {
if gp.status != _Gdead {
markrootStack(gp)
}
}

// 4. 扫描 finalizer、其他运行时根等
markrootFinalizers()
}

// markrootBlock 扫描一块内存中的指针,将指向堆对象的指针入队
func markrootBlock(b, n uintptr, mask *uint8) {
for i := uintptr(0); i < n; i += sys.PtrSize {
if addb(mask, i/8)&(1<<(i%8)) != 0 {
ptr := *(*uintptr)(unsafe.Pointer(b + i))
if ptr != 0 {
shade(ptr)
}
}
}
}

// markrootStack 将 goroutine 栈上存活的指针加入标记队列
func markrootStack(gp *g) {
// 遍历栈帧,根据类型信息找到指针槽并 shade
for frame := getStackMap(gp, nil, true); frame.valid(); frame = frame.next() {
for _, ptr := range frame.ptrMap {
if *ptr != 0 {
shade(*ptr)
}
}
}
}

// gcWorker 是并发标记工作协程的主循环
func gcWorker() {
gp := getg()
gp.gcAssistTime = 0

for {
// 从本地或全局队列取灰色对象
obj := gp.m.p.ptr().gcw.get()
if obj == 0 {
// 队列空,尝试从全局窃取或结束
if gcMarkDone() {
break
}
continue
}

// 扫描该对象:根据类型信息找到内部指针,并 markObject
scanobject(obj, gp.m.p.ptr().gcw)
}
}

// scanobject 扫描对象 obj 内部的所有指针,将引用的堆对象标记并入队
func scanobject(obj uintptr, gcw *gcWork) {
// 根据 obj 的 type 获取 type.gcdata 位图,逐指针扫描
s := spanOf(obj)
if s == nil {
return
}
n := s.elemsize
if n > maxObletBytes {
// 大对象拆成多个 oblet,分次扫描
for oblet := obj; oblet < obj+n; oblet += maxObletBytes {
scanblock(oblet, maxObletBytes, s.type_.gcdata, gcw)
}
return
}
scanblock(obj, n, s.type_.gcdata, gcw)
}

// scanblock 根据 gcdata 位图扫描 [addr, addr+size) 中的指针
func scanblock(addr, size uintptr, gcdata *byte, gcw *gcWork) {
for i := uintptr(0); i < size; i += sys.PtrSize {
if addb(gcdata, i/8)&(1<<(i%8)) != 0 {
ptr := *(*uintptr)(unsafe.Pointer(addr + i))
if ptr != 0 {
base := findObject(ptr)
if base != 0 {
markObject(base, ptr)
}
}
}
}
}

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
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
// sweep 清扫一个 span:回收白色对象,更新 allocBits
func (s *mspan) sweep(preserve bool) bool {
// 1. 原子更新 span 状态(避免多个 P 重复清扫)
if !s.sweepgen.compareAndSwap(s.sweepgen-2, s.sweepgen-1) {
return false
}

// 2. 统计存活对象数,将未标记对象的 allocBits 清零
nalloc := uintptr(0)
for i := uintptr(0); i < s.nelems; i++ {
if s.gcmarkBits.isMarked(i) {
nalloc++
} else {
s.allocBits.clearMarked(i)
}
}

// 3. 用 gcmarkBits 作为新的 allocBits,为下次 GC 准备新的 gcmarkBits
s.allocBits, s.gcmarkBits = s.gcmarkBits, s.allocBits
s.gcmarkBits.clearAll()
s.allocCount = uint16(nalloc)
s.freeindex = 0

// 4. 根据 span 状态归还 mheap 或放回 mcentral
if s.allocCount == 0 {
mheap_.freeSpan(s)
return true
}
if s.allocCount < s.nelems {
mheap_.central[s.spanclass].mcentral.partial[0].push(s)
}
return true
}

// 增量清扫:在分配或后台定期清扫若干 span
func sweepone() *mspan {
for {
s := mheap_.nextSpanForSweep()
if s == nil {
return nil
}
if s.sweep(false) {
return s
}
}
}

// nextSpanForSweep 从清扫列表中取一个待清扫的 span
func (h *mheap) nextSpanForSweep() *mspan {
// 从 mheap.sweepSpans 或 central 中取
// 实现略
return nil
}

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
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
// gcStart 启动新一轮 GC
func gcStart(trigger gcTrigger) {
// 1. 检查是否满足触发条件
if !trigger.test() {
return
}

// 2. STW:停止所有 goroutine
semacquire(&worldsema)
stopTheWorld()

// 3. 标记阶段准备
setGCPhase(_GCmark)

// 4. 启用写屏障(必须在 STW 内开启)
gcBgMarkWorkerPool.reset()
gcMarkRootPrepare()
gcMarkTinyAllocs()

// 5. 初始化每个 P 的 gcWork
for _, p := range allp {
p.gcw.reset()
}

// 6. 根对象入队(标记为灰色)
markRoots()

// 7. 恢复世界,开始并发标记
startTheWorld()
semrelease(&worldsema)

// 8. 启动后台标记 worker
gcBgMarkStartWorkers()
}

STW 时间: 通常 < 1ms

阶段 2: 标记阶段(并发)

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
// gcBgMarkStartWorkers 启动后台标记 worker
func gcBgMarkStartWorkers() {
for _, p := range allp {
if p.gcBgMarkWorker == 0 {
go gcBgMarkWorker(p)
notetsleepg(&work.bgMarkReady, -1)
noteclear(&work.bgMarkReady)
}
}
}

// gcBgMarkWorker 后台标记 worker 主循环
func gcBgMarkWorker(_p_ *p) {
gp := getg()
_p_.gcBgMarkWorker = gp.goid

for {
// 1. 休眠直到被唤醒参与标记
gopark(func() bool {
return atomic.Load(&work.bgMarkReady) != 0
}, unsafe.Pointer(&work.bgMarkReady), waitReasonGCWorkerIdle, traceEvGoBlock, 0)

// 2. 参与标记:从 gcw 取灰色对象并扫描
systemstack(func() {
gcMarkWorker(_p_)
})

// 3. 检查是否进入标记终止
if gcMarkDone() {
break
}
}
}

// gcMarkWorker 执行一轮标记:取灰色对象、扫描、入队
func gcMarkWorker(_p_ *p) {
gcw := &_p_.gcw
for {
obj := gcw.tryGet()
if obj == 0 {
obj = gcw.get()
}
if obj == 0 {
return
}
scanobject(obj, gcw)
}
}

并发执行: GC 与程序同时运行

阶段 3: 标记终止(STW)

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
// gcMarkDone 判断标记是否完成,若完成则进入标记终止
func gcMarkDone() bool {
// 1. 检查所有 P 的本地队列是否都空
for _, p := range allp {
if !p.gcw.empty() {
return false
}
}

// 2. 检查全局队列是否空
if !work.full.empty() {
return false
}

// 3. 进入标记终止
semacquire(&worldsema)
stopTheWorld()

// 4. 再次排空所有 P 的队列(STW 期间可能还有写屏障入队)
for _, p := range allp {
p.gcw.drain()
}
drainWorkbuf()

// 5. 关闭写屏障
setGCPhase(_GCoff)

// 6. 计算清扫相关统计,准备清扫
gcSweepTerminate()

startTheWorld()
semrelease(&worldsema)
return true
}

// drainWorkbuf 排空全局 full 队列
func drainWorkbuf() {
for {
wbuf := trygetfull()
if wbuf == nil {
break
}
for i := uintptr(0); i < wbuf.nobj; i++ {
obj := wbuf.obj[i]
scanobject(obj, getg().m.p.ptr().gcw)
}
putempty(wbuf)
}
}

STW 时间: 通常 < 1ms

阶段 4: 清扫阶段(并发)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// gcSweepTerminate 标记终止时调用,初始化清扫状态
func gcSweepTerminate() {
// 将需要清扫的 span 加入清扫列表
for sweepone() != nil {
// 可在此限制每轮清扫数量,实现增量
}
}

// 清扫在分配路径上增量执行:mallocgc 中若发现 span 待清扫会先 sweep
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// ...
// 若当前 span 需要清扫,先执行 sweepone
if s.needsSweep() {
sweepone()
}
// ...
}

// needsSweep 判断 span 是否在本轮 GC 中需要被清扫
func (s *mspan) needsSweep() bool {
return s.sweepgen == mheap_.sweepgen-1
}

增量执行: 清扫工作分散到多个周期

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
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
// gcTrigger 表示一次 GC 的触发条件
type gcTrigger struct {
kind gcTriggerKind
now int64 // 当前时间(纳秒)
n uint32 // 触发时堆大小等
}

type gcTriggerKind int

const (
gcTriggerHeap gcTriggerKind = iota // 堆大小达到阈值
gcTriggerTime // 定时
gcTriggerCycle // 第 N 轮 GC
gcTriggerAlways // 强制(如 runtime.GC())
)

// test 判断是否应触发 GC
func (t gcTrigger) test() bool {
if t.kind == gcTriggerAlways {
return true
}

if t.kind == gcTriggerHeap {
// 读取当前堆大小
memstats := readmemstats()
heapLive := memstats.heap_live // 当前存活堆大小
heapGoal := memstats.next_gc // 目标堆大小(上次 GC 存活量 × (1 + GOGC/100))

// 当前堆 >= 目标堆 则触发
return heapLive >= heapGoal
}

if t.kind == gcTriggerTime {
// 距离上次 GC 超过一定时间
return lastgc != 0 && t.now-lastgc > forcegcperiod
}

return false
}

// readmemstats 读取内存统计(原子或加锁)
func readmemstats() *memstats {
return &memstats
}

// 在 malloc 路径上检查是否应触发 GC
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// ...
if t := (gcTrigger{kind: gcTriggerHeap}); t.test() {
gcStart(t)
}
// ...
}

代码触发

1
2
3
4
5
// 手动触发 GC
runtime.GC()

// 设置 GC 目标百分比
debug.SetGCPercent(100) // 默认 100

2. 强制触发

内存限制触发

1
2
3
4
// Go 1.19+ 支持内存限制
debug.SetMemoryLimit(2 * 1024 * 1024 * 1024) // 2GB

// 当内存使用超过限制时,强制触发 GC

系统压力触发

当系统内存压力大时,Go 会主动触发更频繁的 GC。

3. 定时触发

Go 运行时可能会定时触发 GC,确保内存及时回收。

GC 性能指标

关键指标

1. GC 频率(GC Frequency)

1
2
3
4
GC 频率 = GC 次数 / 运行时间

频率过高: 可能内存分配过快
频率过低: 可能内存使用过多

2. GC 暂停时间(GC Pause Time)

1
2
3
STW 时间 = 准备阶段时间 + 标记终止时间

目标: < 1ms(通常 < 0.5ms)

3. GC CPU 占用(GC CPU Usage)

1
2
3
GC CPU 占用 = GC 使用的 CPU 时间 / 总 CPU 时间

目标: < 25%

4. 堆内存使用(Heap Usage)

1
2
3
堆内存使用 = 当前堆内存 / 最大堆内存

目标: 合理使用,避免过度分配

查看 GC 统计信息

方式 1: 使用 GODEBUG

1
2
3
4
5
# 设置环境变量
export GODEBUG=gctrace=1

# 运行程序
go run main.go

输出示例:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
"runtime"
"time"
)

func main() {
var m1, m2 runtime.MemStats

runtime.GC()
runtime.ReadMemStats(&m1)

// 执行代码
data := make([]byte, 1024*1024)
_ = data

runtime.GC()
runtime.ReadMemStats(&m2)

fmt.Printf("GC 次数: %d\n", m2.NumGC-m1.NumGC)
fmt.Printf("GC 暂停时间: %v\n", time.Duration(m2.PauseTotalNs-m1.PauseTotalNs))
fmt.Printf("堆内存: %d KB\n", m2.HeapAlloc/1024)
}

方式 3: 使用 pprof

1
2
3
4
5
6
# 获取 GC 统计
go tool pprof http://localhost:6060/debug/pprof/heap

# 查看 GC 相关信息
(pprof) top
(pprof) list 函数名

GC 调优参数

1. GOGC 参数

控制 GC 的触发频率:

1
2
3
4
5
6
7
8
9
10
11
12
# 默认值:100
# 表示:当堆内存增长 100% 时触发 GC
export GOGC=100

# 更积极的 GC(更频繁,内存使用更少)
export GOGC=50 # 增长 50% 就触发

# 更宽松的 GC(更少 GC,内存使用更多)
export GOGC=200 # 增长 200% 才触发

# 禁用自动 GC(不推荐)
export GOGC=off

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
2
3
4
5
6
7
8
# 设置最大内存使用为 2GB
export GOMEMLIMIT=2GiB

# 设置最大内存使用为 512MB
export GOMEMLIMIT=512MiB

# 禁用内存限制(默认)
export GOMEMLIMIT=0

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
2
3
4
5
6
7
8
# GC 调试信息
export GODEBUG=gctrace=1

# 设置 GC 目标百分比(代码中)
debug.SetGCPercent(100)

# 设置内存限制(代码中)
debug.SetMemoryLimit(2 * 1024 * 1024 * 1024)

GC 优化策略

1. 减少内存分配

使用对象池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"sync"
)

var pool = sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}

func main() {
// 从池中获取
buf := pool.Get().([]byte)
defer pool.Put(buf) // 归还到池中

// 使用 buf
// ...
}

预分配切片容量

1
2
3
4
5
6
7
8
9
10
11
// 不好:频繁扩容,增加 GC 压力
var s []int
for i := 0; i < 1000; i++ {
s = append(s, i)
}

// 好:预分配容量
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
s = append(s, i)
}

2. 减少指针使用

使用值类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 小结构体使用值类型
type Point struct {
X, Y int
}

// 值类型:在栈上分配,无需 GC
func useValue(p Point) {
// ...
}

// 指针类型:在堆上分配,需要 GC
func usePointer(p *Point) {
// ...
}

使用数组而非切片

1
2
3
4
5
// 固定大小使用数组
var arr [100]int // 栈上分配

// 动态大小使用切片
var slice []int // 堆上分配

3. 优化数据结构

减少嵌套指针

1
2
3
4
5
6
7
8
9
// 不好:深层指针嵌套
type Node struct {
Children []*Node // 指针数组
}

// 好:使用索引
type Node struct {
Children []int // 索引数组
}

使用结构体数组

1
2
3
4
5
// 不好:指针数组
type Nodes []*Node

// 好:值数组
type Nodes []Node

4. 合理设置 GC 参数

根据场景调整 GOGC

1
2
3
4
5
6
7
8
// 内存受限环境
os.Setenv("GOGC", "50")

// 内存充足环境
os.Setenv("GOGC", "200")

// 容器环境
os.Setenv("GOMEMLIMIT", "512MiB")

5. 监控 GC 性能

定期检查 GC 统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"runtime"
"time"
)

func monitorGC() {
var m runtime.MemStats
for {
runtime.ReadMemStats(&m)
fmt.Printf("GC 次数: %d\n", m.NumGC)
fmt.Printf("GC 暂停时间: %v\n", time.Duration(m.PauseTotalNs))
fmt.Printf("堆内存: %d MB\n", m.HeapAlloc/1024/1024)
time.Sleep(5 * time.Second)
}
}

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
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
package main

import (
"fmt"
"runtime"
"time"
)

func main() {
// 启动监控
go monitorGC()

// 模拟工作负载
for i := 0; i < 1000; i++ {
data := make([]byte, 1024*1024)
_ = data
time.Sleep(100 * time.Millisecond)
}
}

func monitorGC() {
var lastNumGC uint32
var lastPauseTotalNs uint64

for {
var m runtime.MemStats
runtime.ReadMemStats(&m)

if lastNumGC > 0 {
numGC := m.NumGC - lastNumGC
pauseTotal := m.PauseTotalNs - lastPauseTotalNs

if numGC > 0 {
avgPause := time.Duration(pauseTotal) / time.Duration(numGC)
fmt.Printf("GC 次数: %d, 平均暂停: %v, 堆内存: %d MB\n",
numGC, avgPause, m.HeapAlloc/1024/1024)
}
}

lastNumGC = m.NumGC
lastPauseTotalNs = m.PauseTotalNs
time.Sleep(5 * time.Second)
}
}

示例 2: 优化内存分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"sync"
)

// 使用对象池减少分配
var bufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, 0, 1024)
},
}

func processData(data []byte) {
// 从池中获取缓冲区
buf := bufferPool.Get().([]byte)
defer bufferPool.Put(buf[:0]) // 归还到池中,重置长度

// 使用缓冲区
buf = append(buf, data...)
// 处理数据...
_ = buf
}

示例 3: 调整 GC 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"runtime/debug"
"os"
)

func main() {
// 根据环境调整 GC
if os.Getenv("ENV") == "production" {
// 生产环境:平衡内存和性能
debug.SetGCPercent(100)
debug.SetMemoryLimit(2 * 1024 * 1024 * 1024) // 2GB
} else {
// 开发环境:更积极的 GC
debug.SetGCPercent(50)
}

// 业务代码...
}

总结

Go 垃圾回收器的核心特点:

  1. 并发执行: GC 与程序并发运行,减少停顿时间
  2. 三色标记: 使用三色标记算法追踪可达对象
  3. 写屏障保护: 确保并发标记的正确性
  4. 低延迟: STW 时间通常 < 1ms
  5. 自动管理: 无需手动管理内存

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 压力,提高程序性能。


文章作者: djaigo
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 djaigo !
评论
  目录