真正反映进程主动申请堆内存规模的是 malloc/new 累计分配量减去已释放部分,需用 valgrind 的 massif 工具抓取快照;memcheck 的 “still reachable” 不等于无泄漏,长期运行服务中会累积致 oom。

直接看进程 RSS 和堆分配总量,别只盯着 top 的 RES
top 或 ps 显示的 RES(驻留集)包含代码段、栈、共享库和堆,但无法区分哪部分是动态分配的堆内存。真正反映“进程主动申请的堆内存”规模的是 malloc/new 累计分配量减去已 free/delete 的部分——这个值 普通系统命令根本看不到,必须用 Valgrind 的 Massif 工具抓取快照。
常见误判:看到某个进程 RES 持续上涨就断定“堆泄漏”,其实可能是 mmap 分配的大块内存、共享内存段或未归还给操作系统的 brk 剩余空间。这些都不会被 Memcheck 当作泄漏报告,但 Massif 能画出堆增长曲线。
用 Massif 抓堆内存峰值,不是所有场景都适合 --leak-check=full
Memcheck 的 --leak-check=full 适合找“没释放的指针”,但对“短期暴涨后回落”的堆抖动无能为力;而 Massif 才是专盯堆体积变化的工具。运行命令示例:
valgrind --tool=massif --heap=yes --stacks=no --time-unit=B ./your_program
关键参数说明:
-
--heap=yes:必须开启,否则 Massif 只统计栈,不统计堆 -
--stacks=no:关闭栈统计,避免干扰堆数据(除非你真在查栈溢出) -
--time-unit=B:按字节输出,方便 grep 或脚本解析;默认是 I(指令数),完全没意义 - 输出文件默认是
massif.out.<pid></pid>,可用ms_print massif.out.1234生成可读报告
注意:massif.out 里最值得关注的是 heap_alloc_bytes 曲线峰值,以及对应时间点的调用栈——它告诉你“哪个函数调用链吃掉了最多堆内存”,而不是“哪个指针没 free”。
Memcheck 报告里 “still reachable” 不等于没泄漏,但常被忽略
很多人扫一眼 Memcheck 结尾 summary,看到只有 still reachable: 123456 bytes in 1 blocks 就认为“没问题”。错。这表示指针还在作用域内、没被销毁,但程序生命周期结束前本应主动释放——比如全局容器、单例对象持有的缓存、日志缓冲区等。
这类泄漏在长期运行服务(如 IPC 监控进程)中会持续累积,最终触发 OOM killer。修复建议:
- 加
--show-reachable=yes让 Memcheck 显式列出这些块 - 检查对应调用栈:是不是在
main()返回前忘了调用cleanup()? - 确认是否真需要“进程级生命周期”的堆内存;若只是临时缓存,改用栈分配或 scoped 生命周期管理
特别提醒:still reachable 在 fork 子进程后可能变成 definitely lost——因为父进程指针没传给子进程,子进程 exit 时这部分内存就彻底丢了。
Valgrind 运行慢不是 bug,是代价;嵌入式设备上慎用全量检测
Valgrind 插桩导致程序变慢 10–50 倍是常态,尤其带 --track-origins=yes 时。在资源受限的嵌入式 IPC 设备上直接跑 valgrind --tool=memcheck,很可能卡死或触发 watchdog 复位。
实操建议:
- 先在 x86 宿主机上用相同编译选项(
-g -O2)复现问题,定位到可疑模块 - 对目标模块单独编译成独立可执行文件,再用 Valgrind 检测,避免整机启动开销
- 生产环境调试时,用
--log-file=vg.log+--quiet避免终端刷屏,便于重定向分析 - 如果连宿主机都跑不动,换
gperftools的pprof --heap_profile,它开销小得多,虽不如 Valgrind 精确,但能快速暴露增长趋势
真正难的不是跑出报告,而是从几百行堆栈里识别出哪一层调用是设计缺陷、哪一层是第三方库已知行为、哪一层是误报——这需要结合代码上下文反复验证,不能只信 Valgrind 输出的一行地址。











