真实泄漏需满足:稳定负载下inuse_space线性增长且重启归零复现;采样须避开通电初期及GC高频期,两次快照间隔≥180秒,用-base对比inuse_space净增长,禁用?gc=1以防掩盖泄漏。

常驻内存不释放 ≠ 内存泄漏,但 pprof 的 inuse_space 持续增长且重启归零复现,就是真实泄漏的铁证。
怎么确认是真泄漏,不是 GC 假象
很多同学一看到 pprof 里内存数字变大就紧张,其实多数是误报。关键看三点:
- 采样时程序必须处于 稳定负载:刚启动、压测中、或
MemStats.NextGC接近MemStats.HeapAlloc时,GC 频繁,快照全是临时对象 - 两次快照间隔至少 2–3 分钟,且中间无重启、无大流量突变
- 对比指标必须是
-inuse_space(当前驻留内存),不是-alloc_space(历史总分配);如果alloc_space远高于inuse_space,说明分配多、释放快,大概率只是高频分配,不是泄漏
线上验证最稳的方式:用 wget "http://localhost:6060/debug/pprof/heap?inuse_space" -O before.heap,等 180 秒,再抓 after.heap,文件名务必带 .heap 后缀。
为什么 top 看到 *UserConfig 占 150MB,list 却找不到分配点
因为 top 统计的是当前所有存活 *UserConfig 实例的内存总和,而 list 显示的是函数内「所有发生过的分配」(含已被 GC 回收的)。你代码里很可能根本没写 &UserConfig{} —— 它是被间接创建并长期持有的。
- 典型场景:
json.Unmarshal解析进全局map[string]*UserConfig,但 key 永远不删 -
sync.Map.LoadOrStore("id", cfg)后忘记清理过期项 - slice 截取后赋给全局变量:
globalBuf = b[:n],底层数组仍被引用 - HTTP handler 闭包捕获大结构体:
http.HandleFunc("/api", func(w, r) { data := loadBigData(); handle(data) })——data被闭包持有,随每次请求累积
此时必须用 go tool pprof -http=:8080 binary before.heap after.heap,打开后点 View → Difference,再 Focus *UserConfig,才能看到谁在 new 它且没释放。
调用栈全是 runtime.selectgo / runtime.gopark,怎么定位业务代码
这说明泄漏藏在 goroutine 持有引用里,而非函数内直接分配。pprof 默认只显示顶层调用栈,而真正的问题往往卡在中间层。
- 看到
*model.Order占用高,但调用栈只显示http.HandlerFunc.ServeHTTP?那它大概率被 handler 闭包意外捕获了 - 重点关注这些 runtime 符号:
runtime.selectgo(channel 阻塞)、runtime.gopark(goroutine 挂起)—— 它们不是根源,是症状,背后是 channel 接收后存入全局map/slice,或闭包捕获了大结构体 - 用
go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2查看完整 goroutine 栈,找那些状态为chan receive或select且长时间不动的协程
别指望 top 直接给你答案;runtime.mallocgc 占比最高是正常现象,它是所有堆分配的统一入口,真正要盯的是它的调用栈上层。
pprof 默认不开启 heap 采样,第一次访问可能为空
这是最容易被忽略的一点:Go 的 net/http/pprof 默认 **不主动采集 heap profile**,首次访问 /debug/pprof/heap 可能返回空或极小数据。必须让程序先运行一段时间、产生足够堆分配,再抓快照。
- 本地复现更可控:在关键逻辑前后手动调用
pprof.WriteHeapProfile(f),比如 handler 入口/出口、goroutine 启动前后 - 绝对不要加
?gc=1参数——它强制 GC 后采样,会把本该存活的对象“刷掉”,掩盖真实泄漏 - 线上服务建议用 HTTP 接口抓,但需确保端口可访问且有权限限制;
?debug=1仅用于开发排查,文本格式无法做 diff
真正难的从来不是跑通命令,而是理解每个参数背后的运行时语义:inuse_space 是活着的对象,runtime.gopark 是挂起的协程,Focus 是差分后的过滤入口——漏掉任一环,都可能把泄漏当成噪音放过。


















