内存与垃圾回收
Go 的 GC 设计目标:低延迟优先(STW 毫秒级),换吞吐。面试主线:对象去哪、GC 怎么标记、怎么调。
逃逸分析
func f() *int {
x := 1
return &x // x 逃逸: 分配在堆上, 函数返回后仍存活
}- 逃逸分析:编译器判断变量能否留在栈上;取地址返回、传入接口、闭包捕获都会导致逃逸
- 栈上分配:零 GC 开销(随函数退出自动释放);堆上分配:进 GC 管
- 优化:减少逃逸(避免返回指针、避免不必要接口装箱)是 Go 性能调优方向
三色标记
三色标记: 灰是工作队列, 黑是存活, 白是垃圾
- 黑色:已扫描完(存活),灰色:已发现、引用未扫描(工作队列),白色:未发现(候选垃圾)
- 从根(全局、栈、寄存器)出发,灰变黑、引用变灰,直到无灰 → 白色回收
- 并发标记:GC 和业务 goroutine 同时跑,需要写屏障保证不误回收(见下)
混合写屏障
并发 GC 的核心难题:标记期间业务代码改了引用怎么办(黑对象引用了白对象,白对象会被误回收)。
写屏障:对象引用被修改时,拦截并标记新引用(插入屏障)或旧引用(删除屏障)。
Go 1.8+ 用混合写屏障(插入 + 删除):
- 标记和业务并发执行,不需要 STW 全停
- 结合三色不变式:黑色对象不能直接引用白色对象(屏障保证)
- 收益:STW 压缩到毫秒级(标记阶段基本不暂停)
GOGC 调优
| 参数 | 含义 |
|---|---|
| GOGC(默认 100) | 堆增长 100% 触发 GC:上一轮活 10MB,堆到 20MB 触发 |
| GOMEMLIMIT(1.19+) | 内存软上限,减少 GC 频率 |
| debug.SetGCPercent | 运行时调整 |
- GOGC 越大 GC 越少(省 CPU 但内存峰值高),越小越频繁(内存稳但 CPU 换)
- 调优方向:看 pprof 的 heap profile 和 GC 统计(
runtime/metrics) - 面试话术:低延迟场景调大 GOGC(少 GC),内存受限场景调小;1.19 后用 GOMEMLIMIT 更平滑
面试追问
- 逃逸分析是什么? 编译器判断变量分配栈还是堆。返回指针/接口装箱/闭包捕获会逃逸
- 三色标记? 白(候选垃圾)灰(待扫描)黑(存活)。从根出发标记,无灰后清白
- 写屏障解决什么? 并发标记时引用被改,防误回收。Go 用混合写屏障,STW 毫秒级
- GOGC 怎么调? 堆增长阈值(默认 100%)。大 → 少 GC 高内存,小 → 反之。GOMEMLIMIT 更平滑
- Go GC 的目标? 低延迟优先:STW 毫秒级,牺牲部分吞吐。和 Java G1 思路类似