Go Memory Model and GC
1. Memory Allocation Basics
1.1 Stack and Heap
┌─────────────────────────────┐
│ 堆 (Heap) │ ← 动态分配,GC 管理
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ obj │ │ obj │ │ obj │ │
│ └─────┘ └─────┘ └─────┘ │
├─────────────────────────────┤
│ goroutine 栈 1 │ ← 2KB 起始,自动增长
│ ┌─────────────────────┐ │
│ │ 局部变量、函数参数 │ │
│ └─────────────────────┘ │
├─────────────────────────────┤
│ goroutine 栈 2 │
│ ┌─────────────────────┐ │
│ │ 局部变量、函数参数 │ │
│ └─────────────────────┘ │
└─────────────────────────────┘
- Stack allocation: Extremely fast (only needs to move the stack pointer), automatically reclaimed when function returns
- Heap allocation: Requires GC for reclamation, has overhead
- The Go compiler decides whether variables are allocated on the stack or heap through escape analysis
1.2 Escape Analysis
# 查看逃逸分析结果
go build -gcflags="-m" main.go
go build -gcflags="-m -m" main.go # 更详细
// 不逃逸:局部变量,分配在栈上
func noEscape() int {
x := 42
return x // 值拷贝,x 不逃逸
}
// 逃逸:返回指针,分配在堆上
func escape() *int {
x := 42
return &x // x 逃逸到堆,因为函数返回后还要用
}
// go build -gcflags="-m" 输出: moved to heap: x
// 逃逸:接口类型参数
func printVal(v interface{}) {
fmt.Println(v) // v 逃逸(interface{} 导致)
}
// 逃逸:闭包引用
func closure() func() int {
x := 0
return func() int {
x++ // x 被闭包捕获,逃逸到堆
return x
}
}
// 逃逸:发送到 channel
func chanEscape(ch chan *int) {
x := 42
ch <- &x // x 逃逸
}
// 不逃逸:已知大小的切片
func noEscapeSlice() {
s := make([]int, 10) // 不逃逸(编译期已知大小)
_ = s
}
// 逃逸:运行时大小的切片
func escapeSlice(n int) {
s := make([]int, n) // 逃逸(编译期不知道大小)
_ = s
}
1.3 When Variables are Allocated on the Heap
| Scenario | Escapes? |
|---|---|
| Returning a pointer to a local variable | Yes |
| Assigned to interface{} | Yes |
| Closure captures local variable | Yes |
| Sending a pointer to a channel | Yes |
| Slice/map runtime size | Possibly |
| Object exceeding a certain size | Yes |
| Pure value type local variable | No |
| Slice of compile-time known size | No |
2. Go Memory Allocator
Go's memory allocator is based on the tcmalloc (Thread-Caching Malloc) philosophy.
2.1 Three-Tier Structure
goroutine ──► mcache (每个P一个,无锁)
│
▼
mcentral (全局,有锁,按 size class)
│
▼
mheap (全局,向 OS 申请)
│
▼
操作系统 (mmap/sbrk)
- mcache: Each P has one, no locking required when allocating small objects
- mcentral: When mcache is exhausted, objects are obtained from mcentral, which requires locking
- mheap: When mcentral is also insufficient, memory is requested from mheap, which in turn requests memory from the OS
2.2 mspan and Size Class
Go categorizes objects into approximately 70 size classes (8B, 16B, 32B, 48B, ...32KB) based on their size.
- Tiny objects (<16B): Use tiny allocator, multiple small objects can share a memory block
- Small objects (16B-32KB): Allocated from mcache according to size class
- Large objects (>32KB): Allocated directly from mheap
// 观察分配行为
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("堆分配次数: %d\n", m.Mallocs)
fmt.Printf("堆释放次数: %d\n", m.Frees)
fmt.Printf("堆使用字节: %d\n", m.HeapAlloc)
fmt.Printf("系统内存: %d\n", m.Sys)
3. Garbage Collection (GC)
3.1 Tri-color Marking Algorithm
Go uses a concurrent tri-color mark-and-sweep algorithm.
白色(White): 未被标记,GC 结束后回收
灰色(Gray): 已标记但子对象未扫描
黑色(Black): 已标记且子对象已扫描,保留
标记过程:
1. 所有对象初始为白色
2. 从根对象(栈、全局变量)出发,标记为灰色
3. 取出一个灰色对象,扫描它引用的所有对象:
- 白色 → 标记为灰色
- 已经是灰色或黑色 → 跳过
- 当前对象标记为黑色
4. 重复步骤3,直到没有灰色对象
5. 剩余的白色对象就是垃圾,回收
初始: [白A] → [白B] → [白C]
↗
[白D] → [白E]
Step1: [灰A] → [白B] → [白C] (A 是根对象)
↗
[灰D] → [白E] (D 是根对象)
Step2: [黑A] → [灰B] → [白C] (扫描A,B变灰)
↗
[黑D] → [灰E] (扫描D,E变灰)
Step3: [黑A] → [黑B] → [灰C] (扫描B,C变灰)
↗
[黑D] → [黑E] (扫描E,C已灰)
Step4: [黑A] → [黑B] → [黑C] (扫描C,完成)
[黑D] → [黑E]
→ 没有白色对象,全部保留
3.2 Write Barrier
When GC runs concurrently with user code, the user might modify reference relationships. Write barriers ensure that live objects are not missed.
Go 1.8+ uses a Hybrid Write Barrier:
- At the start of marking, all objects on the stack are marked black
- Newly created objects on the heap are marked black
- Objects pointed to by overwritten pointers are marked gray
3.3 GC Trigger Conditions
// 1. 堆内存增长到上次 GC 后的 GOGC% 时触发(默认 100%,即翻倍)
// 环境变量控制
GOGC=100 // 默认,堆增长 100% 时触发
GOGC=200 // 堆增长 200% 时触发(GC 频率降低,内存占用增加)
GOGC=50 // 堆增长 50% 时触发(GC 更频繁,内存占用降低)
GOGC=off // 关闭 GC
// 2. 定时触发:距离上次 GC 超过 2 分钟
// 3. 手动触发
runtime.GC()
3.4 GOMEMLIMIT (Go 1.19+)
Soft memory limit, when approaching the limit, GC will reclaim more aggressively.
// 环境变量
GOMEMLIMIT=1GiB // 软限制 1GB
GOMEMLIMIT=512MiB
// 代码中设置
import "runtime/debug"
debug.SetMemoryLimit(1 << 30) // 1GB
// 典型用法:容器环境
// 容器限制 2GB,设置 GOMEMLIMIT=1.5GiB
// 留 0.5GB 给非堆内存(栈、goroutine等)
3.5 GC Phases
用户代码运行 │ STW │ 并发标记 │ STW │ 并发清除 │ 用户代码运行
│Mark │ │Mark │ │
│Setup│ │Term │ │
│ │ │ ││
<1ms <1ms
- Mark Setup (STW): Enable write barrier, usually <1ms
- Concurrent Marking: Runs in parallel with user code
- Mark Termination (STW): Disable write barrier, usually <1ms
- Concurrent Sweeping: Reclaims white objects
4. Common Memory Leak Scenarios
4.1 goroutine Leaks
// 泄漏:goroutine 永远阻塞在 channel
func leak() {
ch := make(chan int)
go func() {
val := <-ch // 永远收不到数据,goroutine 泄漏
fmt.Println(val)
}()
// 函数返回,ch 没人发数据,goroutine 永远不会结束
}
// 修复:使用 context 取消
func noLeak(ctx context.Context) {
ch := make(chan int)
go func() {
select {
case val := <-ch:
fmt.Println(val)
case <-ctx.Done():
return // 可以退出
}
}()
}
4.2 time.After Used in a Loop
// 泄漏:每次循环创建一个 Timer,直到触发才会被 GC
func leak() {
ch := make(chan int)
for {
select {
case v := <-ch:
fmt.Println(v)
case <-time.After(5 * time.Second): // 每次循环都新建 Timer
fmt.Println("timeout")
}
}
}
// 修复:使用 time.NewTimer 并手动 Reset
func noLeak() {
ch := make(chan int)
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
for {
timer.Reset(5 * time.Second)
select {
case v := <-ch:
fmt.Println(v)
case <-timer.C:
fmt.Println("timeout")
}
}
}
4.3 Slice Underlying Array Not Released
// 泄漏:切片引用大数组的一小部分
func leak() []byte {
data := make([]byte, 1<<20) // 1MB
// ... 填充数据
return data[:10] // 返回 10 字节,但底层 1MB 数组不会被回收
}
// 修复:拷贝需要的数据
func noLeak() []byte {
data := make([]byte, 1<<20)
result := make([]byte, 10)
copy(result, data[:10])
return result
}
4.4 Global Variable Holding References
// 泄漏:全局 map 不断增长
var cache = make(map[string][]byte)
func processRequest(key string, data []byte) {
cache[key] = data // 只增不删,内存不断增长
}
// 修复:设置过期策略或大小限制
// 使用 LRU cache 或定期清理
5. Common Functions in the runtime Package
package main
import (
"fmt"
"runtime"
"runtime/debug"
)
func main() {
// CPU 和 Goroutine 信息
fmt.Println("CPU 核数:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
fmt.Println("goroutine 数:", runtime.NumGoroutine())
fmt.Println("Go 版本:", runtime.Version())
// 内存统计
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("堆分配: %d MB\n", m.HeapAlloc/1024/1024)
fmt.Printf("系统内存: %d MB\n", m.Sys/1024/1024)
fmt.Printf("GC 次数: %d\n", m.NumGC)
fmt.Printf("上次 GC 暂停: %d μs\n", m.PauseNs[(m.NumGC+255)%256]/1000)
// 手动触发 GC
runtime.GC()
// 设置 GC 百分比(等价于 GOGC 环境变量)
debug.SetGCPercent(200) // 堆增长 200% 时触发 GC
// 设置内存限制(Go 1.19+)
debug.SetMemoryLimit(512 << 20) // 512MB
// 释放未使用的内存给 OS
debug.FreeOSMemory()
// SetFinalizer: 对象被 GC 回收前执行清理
// 注意:不保证一定执行,不要用于关键清理
type Resource struct{ Name string }
r := &Resource{"test"}
runtime.SetFinalizer(r, func(r *Resource) {
fmt.Println("清理资源:", r.Name)
})
}
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/6766