学习目标
- 理解 Go 的内存分配模型
- 区分栈(stack)与堆(heap)的职责
- 理解 Go GC 的基本原理与运行阶段
- 建立“面向 GC 编程”的工程意识
一、为什么需要理解内存与 GC
在 Go 中:
- 你几乎不需要手动释放内存
- 但这并不意味着内存是“无成本的”
真实工程中的问题往往表现为:
- CPU 使用率异常升高
- 延迟抖动(RT 不稳定)
- 内存持续增长(看似“泄漏”)
工程认知:很多性能问题,本质是 GC 压力问题。
二、栈(stack)与堆(heap)的基本区别
栈的特点:
- 函数调用自动分配
- 分配和回收成本极低
- 生命周期清晰(随函数结束)
堆的特点:
- 跨函数、跨 goroutine 生命周期
- 由 GC 管理
- 分配和回收成本更高
核心目标:
让更多对象分配在栈上,而不是堆上。
三、逃逸分析(escape analysis)
Go 编译器会通过逃逸分析决定:
- 一个对象是否可以安全地放在栈上
- 还是必须逃逸到堆上
示例:
func foo() *int {
x := 10
return &x
}说明:
- x 的地址被返回
- x 的生命周期超出函数
- 必须分配到堆上
查看逃逸分析:
go build -gcflags="-m"四、常见导致逃逸的场景
- 返回局部变量指针
- interface{} 承载具体值
- 闭包引用外部变量
- map / slice 存储指针或大对象
工程结论:逃逸并不一定是坏事,但大量逃逸会增加 GC 压力。
五、Go GC 的整体设计目标
Go 的 GC 目标不是“回收最快”,而是:
- 低延迟(Low Latency)
- 可预测(Predictable)
- 并发执行(Concurrent)
这也是为什么 Go 选择:
- 并发 GC
- 三色标记算法
六、三色标记法(简化理解)
GC 过程中,对象被分为三类:
- 白色:未访问,可能被回收
- 灰色:已发现,但其引用尚未扫描
- 黑色:已扫描,且其引用都已处理
GC 的目标是:
- 从根对象出发
- 标记所有可达对象为黑色
- 剩余白色对象被回收
工程理解即可:GC 是“找还在用的”,不是“找不用的”。
七、并发 GC 与 STW(Stop-The-World)
Go GC 是并发的,但并非完全没有 STW。
GC 阶段简化为:
- 短暂 STW(准备阶段)
- 并发标记(与业务 goroutine 同时运行)
- 短暂 STW(结束阶段)
工程结论:
- 现代 Go 的 STW 非常短
- 但频繁 GC 仍然会影响性能
八、GC 触发条件与 GOGC
GC 触发的核心条件:
- 堆内存增长到一定比例
GOGC 控制 GC 频率:
export GOGC=100含义:
- 堆增长 100% 时触发 GC
- 值越小,GC 越频繁
- 值越大,占用内存越多
工程建议:
- 默认值通常是合理的
- 不要盲目调大或调小
九、常见 GC 压力来源
- 大量短生命周期对象
- 频繁分配大对象
- 过多逃逸到堆
- 不必要的 slice / map 扩容
工程方向:
- 对象复用(sync.Pool)
- 减少分配次数
- 减少临时对象
十、面向 GC 的编程原则
- 减少不必要的堆分配
- 避免创建大量短命对象
- 关注接口与逃逸
- 不要过早优化,但要有意识
工程结论:性能优化的第一步,往往是减少分配。
十一、内存与 GC 总结
- 栈分配几乎是“免费的”
- 堆分配会带来 GC 成本
- GC 不是问题,GC 压力才是问题
一句话总结:
写得越“省内存”,GC 越“省心”。
预告:Day 22|性能分析工具(pprof / trace)
下一天将系统讲解:
- pprof 的使用方式
- CPU / 内存分析
- trace 的调度分析能力
- 如何用工具而不是直觉做优化