登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go heap profile inuse_space 与 alloc_space 的选择

来源:17golang原创

时间:2026-10-01 19:53:02 350浏览 收藏

选择很直接:怀疑内存泄漏、缓存没有淘汰或对象被长期引用时,先看 inuse_space;进程内存相对稳定,但 GC CPU 高、分配速率大或吞吐下降时,先看 alloc_space。前者回答“现在还有多少字节活着”,后者回答“到目前为止一共分配过多少字节”,两者针对的是不同瓶颈。

快速选择
症状优先视图要找的代码
多轮 GC 后堆仍持续抬升inuse_space长期引用、大对象、无上限缓存
内存能回落但 GC 很频繁alloc_space短命对象、重复转换、临时缓冲区
对象数量异常多但单个很小inuse_objects 或 alloc_objects按对象数排序后的热点

请求量上来后,为什么一个 heap profile 会给出两种答案

在低负载环境里,一个每次请求临时分配 200 KB、随后很快被回收的路径可能不明显。流量扩大后,它会持续推高分配速率和垃圾回收频率,却不一定让 GC 后的存活堆显著增长。另一类问题恰好相反:某个缓存或引用链每分钟只增长一点,但对象长期存活,最终形成稳定的内存压力。

这也是只说“看 heap profile”还不够的原因。Go 的 heap profile 同时保留活对象分配点和程序启动以来的历史分配点,pprof 通过不同 sample type 把同一份采样数据切成不同观察面。默认 heap 视图是 inuse_space,它对留存问题很合适,却可能把已经回收的高频短命分配隐藏掉。

默认视图为什么会漏掉分配抖动

inuse_space 按仍存活对象的采样字节数排序。一个函数即使累计分配了数十 GB,只要对象很快回收,在当前快照里也可能不显眼。反过来,一个只分配过一次但长期持有的大切片,会在 inuse_space 中非常突出。因此“top 表里没有热点”不等于“这个路径没有内存成本”,它只说明当前选择的统计口径没有把该成本排到前面。

官方文档还说明,heap profile 反映的是最近一次已完成 GC 时的统计,并会省略更晚的分配,以免结果过度偏向尚未判断是否存活的新对象。它是采样并缩放后的 Go 堆分配视图,不等同于操作系统看到的整个进程 RSS,也不包含所有非 Go 堆内存。

把同一份 heap profile 当作两张视图

inuse_space 与 alloc_space 都来自分配调用栈,区别在于聚合口径。前者统计当前仍在使用的采样字节,后者统计累计分配字节,包括后来已经被垃圾回收的对象。对应的 inuse_objects 和 alloc_objects 则按对象个数而不是字节数观察,适合发现大量小对象。

Go heap profile 中 GC 快照、分配调用栈和四种 sample type 的静态结构图
图1:结构示意图展示同一份 heap profile 中当前存量与历史累计两组采样视图。

/debug/pprof/heap 默认展示 inuse_space;/debug/pprof/allocs 使用相同的底层 profile 数据,但默认 sample index 改为 alloc_space。不必为了切换视图重复采集文件,也可以在 go tool pprof 启动时显式指定 sample index。

# 查看当前仍存活的采样字节,优先定位长期持有路径。
go tool pprof -sample_index=inuse_space heap.pb.gz

# 切换到累计分配字节,优先定位高频短命分配路径。
go tool pprof -sample_index=alloc_space heap.pb.gz

内存回收后仍降不下来,先看 inuse_space

当服务在相同负载下经历多轮 GC 后,存活堆仍阶梯式上升,inuse_space 更接近要回答的问题。先看 top 中 flat 值较高的分配点,再用 list 或调用图确认对象为何仍被引用。常见方向包括没有容量上限的 map、迟迟不清理的缓存、切片持有大底层数组、队列消费者落后以及 goroutine 闭包捕获大对象。

采集前通过 gc=1 请求一次 GC,可以让当前存量视图更接近回收后的状态,但它会给运行中的服务增加额外 GC 开销,不应被高频轮询。更稳妥的做法是在同类负载下间隔采集多份 profile,观察相同调用栈的留存是否持续增加。

# 低频诊断时先触发一次 GC,再读取当前存量视图。
go tool pprof 'http://127.0.0.1:6060/debug/pprof/heap?gc=1'

堆能回落但 GC 很忙,改看 alloc_space

如果 GC 后的存活堆比较平稳,而 CPU profile 或运行时指标显示 GC 成本升高,alloc_space 通常更有价值。它会把已经回收的对象也计入累计分配,能够暴露字符串与字节切片反复转换、循环内临时对象、频繁扩容、每次请求重建编码器等热点。

累计值会随着进程运行时间增长,因此不同启动时长的实例不能直接比较绝对值。HTTP pprof 对 heap 和 allocs 支持 seconds=N 的增量 profile;在一段稳定负载窗口内采集增量数据,更适合比较改动前后的分配成本。

# 采集 30 秒增量 heap profile,避免长期累计值掩盖当前负载。
curl -o heap-30s.pb.gz 'http://127.0.0.1:6060/debug/pprof/heap?seconds=30'

# 按窗口内累计分配字节查看热点。
go tool pprof -sample_index=alloc_space heap-30s.pb.gz

关键取舍:字节数、对象数和采样误差

space 口径容易突出少量大对象,objects 口径更容易突出大量小对象。定位大缓冲区和大切片时先看字节;定位装箱、节点对象或小结构体风暴时再切对象数。两组视图都来自采样,不要把 profile 中的单个数值当作精确账本;热点的相对排序、跨时间变化和代码归因通常更重要。

调整 runtime.MemProfileRate 可以改变采样频率,但更高采样率会增加运行开销,且该值应尽早设置并在进程生命周期中保持一致。多数线上排查先使用默认采样率就够了,只有热点过小且能控制实验成本时才考虑专门调整。

上线后不要只盯一张 top 表

规模化服务里,profile 只是定位线索。判断留存问题时,把 inuse_space 与 GC 后堆大小、对象存活趋势放在一起;判断分配压力时,把 alloc_space 与分配速率、GC CPU、暂停和吞吐放在一起。代码优化后应在相同请求结构和相近负载窗口重新采集,而不是拿两个运行时长不同的累计 profile 比总量。

服务负载、运行时指标、heap profile 两种视图与长期持有和短命高频对象的静态关系图
图2:关系示意图将运行时症状分别映射到留存热点与分配热点,避免用单一视图解释所有内存问题。

选择清单

  • 看“现在还占着多少内存”:选 inuse_space。
  • 看“这段时间制造了多少内存流量”:选 alloc_space,优先使用增量 profile。
  • 大对象不明显但对象数异常:切到 inuse_objects 或 alloc_objects。
  • 比较前确认负载、采样窗口和进程启动时长可比。
  • 不要用 Go heap profile 单独解释 RSS、cgo 内存或内存映射文件。

相关问题

heap 与 allocs endpoint 是两份不同数据吗?

不是。官方文档说明 allocs profile 与 heap profile 相同,主要区别是默认展示 alloc_space,而 heap 默认展示 inuse_space。

查内存泄漏时要不要看 alloc_space?

可以作为补充,但首选仍是 inuse_space。泄漏关注对象为何持续存活;累计分配高只能说明创建得多,不能证明没有被回收。

为什么 profile 与监控里的进程内存对不上?

heap profile 是 Go 堆分配的采样视图,进程内存还可能包含运行时元数据、线程栈、cgo、内存映射和未立即归还给操作系统的页,两者口径不同。

参考:https://pkg.go.dev/runtime/pprof、https://pkg.go.dev/net/http/pprof、https://github.com/google/pprof/blob/main/doc/README.md。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>