很多朋友来问 Linux 内存泄漏排查开口往往是同一句话“我拿 Valgrind 跑过了没报 definitely lost内存怎么还在涨”或者反过来“perf 不是号称能分析内存吗为什么抓不到泄漏点”问得多了我发现问题多半出在一个前提上大家把 Valgrind、perf、Heap Profiler 当成了可以互相替换的同一种工具。实际上这三者在 Linux 内存泄漏定位体系里各站一层原理天差地别适合的场景也完全不一样。这篇附录想做的事情很简单把三个工具放到同一张坐标系里讲清楚它们各自在查什么、怎么查、能查到什么程度最后给出一条从发现内存水位上涨到钉死泄漏代码的完整路径。这篇文章更偏“方法论 实操命令”而不是某个工具的手册原因是工具本身会迭代排查思路才是能反复复用的东西。1. 先搞清楚三个工具分别站在哪一层1.1 为什么要先看原理层级内存泄漏定位这件事麻烦在于“内存泄漏”本身是一个很宽泛的结果描述而不是一个标准化的错误类型。对很多人来说Leak 就是“内存一直涨、最终 OOM”。但落到操作系统层面导致内存涨的原因可能是堆内存没释放、可能是缓存层主动不归还、可能是 mmap 段持续扩大、可能是线程栈反复创建销毁、甚至可能是分配器自己把空闲块留在缓存里不还给内核。不同原因只有不同工具能观测到。perf、Valgrind、Heap Profiler 三者的根本差异不是“哪个更准”而是观测粒度不同perf 站在内核视角不碰你的程序只采样事件。Valgrind 站在模拟器视角把程序整个“囚禁”起来逐指令监视。Heap Profiler 站在分配器视角替换掉 malloc 层记录每一次分配的调用栈和大小。搞清楚这一点后面所有命令和结论就顺理成章了。1.2 perf内核视角的“录像机”perf 是基于内核 perf_event 子系统的性能采样工具。它不注入你的进程不替换任何函数也不改变程序的执行路径。它做的事情是在内核里按一定频率采集硬件计数器、软件事件、tracepoint再把采样到的指令地址或调用栈输出。对于内存问题perf 最常见的用法是采集缺页事件page-faults、内存访问延迟事件、或者通过 perf mem 对访存指令做采样。它能告诉你的是在时间维度上系统或进程发生了多少次缺页缺页时 CPU 正在执行哪个函数。这个信息很有价值但它不能直接告诉你“这里有 10 个字节没人释放”。我用一个类比形容 perf它在十字路口装了一个全景摄像头能精确记录每辆车经过的时间和方向但它不会替你去查某辆车的驾照和违章记录。它适合做整体流量的管控和嫌疑区域的缩小。1.3 Valgrind把程序放进显微镜下观察Valgrind 的 Memcheck 工具是另一个极端。它不借用内核机制而是动态二进制插桩——程序跑的不是原生机器码而是先被 Valgrind 翻译成中间表示在每条指令周围插入检查逻辑同时用自己的内存管理模块替换掉 malloc/free。换句话说Valgrind 对程序做了全量、逐指令的内存状态跟踪。哪块内存分配了、有没有写越界、free 之后有没有再访问、程序退出时哪些内存还活着且没有指针指向它们——它都清清楚楚。代价也很明显程序跑在 Valgrind 上相当于套了个翻译层速度慢 20 到 50 倍是常态而且对运行环境要求苛刻几乎不可能用在线上。它适合的是拿到一个相对可控的复现场景把代码放到显微镜底下做最终定罪。1.4 Heap Profiler把分配器换成有计数的货架Heap Profiler 一般指的是 Google gperftools原 tcmalloc里的 heap profiler和 Valgrind 一样在用户态工作但它没有做指令级插桩而是选择了一个更实用的切入点替换掉 malloc 和 free在每次分配时记录调用栈、线程信息、分配大小并定期或按一定条件导出堆快照。从效果上看它像是给仓库里每个货架装了一个计数器。哪个部门领走的货多、按什么频率领、用了之后是归还还是压箱底都能统计出来。而且它不像 Valgrind 那样把一个程序拖慢几十倍短时间挂在预发或低峰期服务上可接受度远比 Valgrind 高。启动方式也很灵活可以通过链接 libtcmalloc 的方式静态替换也可以用 LD_PRELOAD 在启动时加载。不需要改代码这对排查存量服务很有用。2. perf 实战定位内存泄漏不直接报错但能锁定战场2.1 核心思路先确认内存增长形态很多人一上来就想拿 perf 抓“泄漏调用栈”这个预期本身就是错的。perf 不是拿来直接出结论的工具它的价值是“缩小范围”——先帮你确定泄漏是否真实存在、大致分布在哪个模块、是大块映射还是小块堆分配。我一般建议先从最土的方法开始连续观察进程内存状态。while true; do grep -E VmRSS|VmData|VmSize /proc/pid/status sleep 10 done把几组数据画出来先回答三个问题内存是否在持续增长还是只在高水位横盘。RSS 涨得快还是 VmData/VmSize 涨得快。如果你手动结束进程、重启之后内存是否回落。如果内存涨一小段就稳定很可能是分配器缓存或者阶段性负载问题如果单调递增不回头才值得动用后面的采样工具。这里最容易踩的坑是把 tcmalloc/jemalloc 的“进程高水位”当成泄漏它们在内存池策略上本来就倾向于保留空闲页不还给内核。2.2 用 perf stat 观察缺页事件是否同步增长确认完进程内存曲线后下一步是做事件观测。perf stat -e page-faults,minor-faults,major-faults -p pid -- sleep 60这条命令会在 60 秒内统计该进程的所有缺页事件。如果内存曲线持续上涨同时 minor-faults 也随时间线性增长说明有持续的、新的匿名页被分配并触碰。这里可以做一个简单的换算一次 minor fault 通常意味着触达一个之前未触碰的页如果每秒几百次缺页且每次分配 4KB 匿名页那么一个小时内新增可达几百 MB基本可以确认存在分配行为在持续发生。但如果缺页事件很少内存却仍然上涨那问题往往不在常规 malloc而在 mmap 大块映射、文件页缓存、Shared Memory 或者显存/驱动映射。此时你再拿用户态堆分析工具去查大概率一无所获因为方向就错了。2.3 抓缺页调用栈锁定嫌疑函数确认缺页伴随增长后下一步是用采样记录调用栈sudo perf record -e page-faults -g -p pid -- sleep 30 sudo perf reportperf record 会在每个缺页事件发生时记录当前的函数调用栈。跑完看 callchain通常会看到某些函数路径频繁出现比如正在解析配置、正在构造缓存对象、正在字符串拼接。这些栈不是泄漏本身但它是泄漏发生现场。我遇到过一个案例进程 RSS 持续上涨perf 抓到 page-fault 几乎都落在某个 xml 解析函数上紧接着往里查才发现是半结构化日志解析后生成的对象没有进入销毁队列。perf 没直接告诉我“这里泄漏了”但它把搜索范围从一个几千行代码的服务缩小到了几十行调用栈。这里要提醒一句perf record -e page-faults的采样开销要比普通 cpu-clock 大不少不要挂太久一般抓几十秒就够了。另外云主机和部分虚拟机环境下 perf_event 权限可能受限如果报Permission mmap之类的错误检查/proc/sys/kernel/perf_event_paranoid必要时换一台物理机或者退回用 /proc 脚本采集数据。2.4 perf 的边界边界在哪里perf 能解决“从无到有、从大到小”的问题但有两件事它做不到它不能告诉你某个内存块是否还被指针引用也就无法定义严格意义上的“泄漏”。它不能提供分配/释放的配对信息所以看到某个函数频繁分配可能是正常流量也可能是泄漏光靠 perf 伤难以下最终结论。所以我把 perf 定位成“侦察兵”。它的产出是一份有嫌疑的地图而不是一张起诉书。3. Valgrind 精确到字节的插桩式排查3.1 Memcheck 到底是怎么工作的Valgrind 的 Memcheck 常被直接称为“Valgrind”这让很多人误会它是一个通用性能工具。其实 Valgrind 是框架Memcheck 是它最常用的内存检查工具。Memcheck 截获程序对内存的每一次读、写、分配、释放并且维护两块元信息一块记录每个字节是否可读/可写另一块记录每个字节之前的状态。程序退出时它再扫一遍堆里的内存块检查是否还有没被 free 的块并且通过扫描栈和全局变量看这些内存块还有没有指针引用。如果既没释放又没引用就报告为 definitely lost。正是这种“逐字节跟踪 引用可达性分析”的机制让 Valgrind 成为三者中唯一能给出“确定泄漏”结论的工具。它对 definite lost 的判断非常硬不会因为采样覆盖率不足而漏检因为它是全量检查。3.2 一个完整的排查命令组合假设我们有一个最简单的泄漏程序#include stdlib.h #include unistd.h void make_leak(void) { char *p (char *)malloc(1024); } int main(void) { for (;;) { make_leak(); usleep(100000); } }用 Valgrind 跑这个程序最常用的命令是valgrind --leak-checkfull --show-leak-kindsall --track-originsyes ./leak_demo各选项我一般这样选--leak-checkfull让程序结束时不只输出汇总而是输出每个泄漏点详细调用栈。--show-leak-kindsall展示 definite、indirect、possible、still reachable 四类详情避免漏掉可疑情况。--track-originsyes可以追踪未初始化值的来源排查问题时会一并带上。跑完后的报告里会出现一段类似这样的关键信息1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1 at 0x484A2A3: malloc (vg_replace_malloc.c:423) by 0x109156: make_leak (leak_demo.c:5) by 0x109175: main (leak_demo.c:10)这里的make_leak到main就是完整证据链。拿到这组栈接着去代码里查为什么分配后没有保存指针即可。如果你已经有比较有把握的怀疑函数希望 Valgrind 在出错时立刻让你定位可以加--error-exitcode1让 CI 或自动化脚本在发现泄漏时非零退出构建直接失败。3.3 读报告definitely lost 之外那些项怎么办新手最常见的困惑是看到still reachable和possibly lost就慌了。我的处理习惯如下definitely lost真正的泄漏必须处理。indirect lost跟随 definitely lost 的结构体里继续泄漏通常和前者同一根源。possibly lost指针可能还被存在某个寄存器或某个被覆盖的局部变量里Valgrind 无法完全确定。如果发生在自定义内存池、位压缩指针场景容易出现误报需要人工看代码。still reachable程序退出时仍然有指针指向这块内存只是没释放。严格来说这不是泄漏因为理论上还能访问到。但如果它是全局容器里的缓存没有做退出清理也不算大问题。实际排查时我会优先处理 definitely lost修完再重跑一遍。still reachable 是否处理取决于你的出口路径是否安全。3.4 Valgrind 的代价和限制Valgrind 不是万能药代价非常明显慢。同一个程序跑在 Memcheck 下通常慢 20 到 50 倍。对于一个原本要跑 1 小时的批量任务你可能要等大半天。不适合线上。靠 Valgrind 直接挂在生产进程上不可行变相改变程序时序也可能掩盖与竞态相关的内存问题。fork/exec 的子进程默认不检查。如果你的泄漏发生在客户端创建的子进程里要加--trace-childrenyes。大内存程序会顶不住。Valgrind 需要大量额外内存来维护元数据本身可能触发内存不足。对系统调用直接分配的内存、驱动映射、硬件缓冲区域基本看不到。所以 Valgrind 的正确使用姿势是在可控的测试环境和插桩环境里复现问题用性能换确定性把最后那道“锤子”砸下去。4. Heap Profiler线上友好型快照对比4.1 原理替换 malloc 和堆快照gperftools 的 Heap Profiler 为了兼容线上环境选择了“替换 malloc”而不是“模拟指令执行”。它包装了 malloc/free/realloc在分配入口记录调用栈和大小同时维护当前存活堆的统计信息。启动时通过环境变量控制快照策略比如LD_PRELOAD/usr/lib/libtcmalloc.so.4 \ HEAP_PROFILE_ALLOCATION_INTERVAL1073741824 \ HEAP_PROFILE_TIME_INTERVAL60 \ ./your_service这里的含义是每分配 1GB 内存或每经过 60 秒生成一个堆 profile 文件。你也可以用HEAP_PROFILE_MMAP1追踪 mmap 大块映射。生成的heap.0001.heap文件保存的就是该时刻所有存活分配的调用栈统计快照。4.2 用 pprof 看快照拿到的 profile 是一堆带调用栈统计的数据没法直接读要用 pprof 分析。现代环境我一般用go tool pprof它向下兼容 gperftools 生成的 profile。go tool pprof -top ./your_service heap.0001.heap输出里会按“当前存活空间”把调用栈排出来和 CPU profile 的 top 排序类似。占用最多空间的函数、库、模块会一目了然。如果只看总体趋势不关心具体栈可以先用 pprof 的文本模式过一遍go tool pprof -text ./your_service heap.0001.heap一般来说top 里长期占据前列的分配栈就有重大嫌疑。但注意只看单个快照容易把“瞬时峰值”当成“泄漏增长”真正要做的操作是快照对比。4.3 快照对比是抓泄漏增长的关键操作内存泄漏定位应该看“增长”而不是看“存量”。一个正常服务也可能在启动后缓存大量数据单看某一时刻的 heap profile内存占用极高不代表泄漏。正确做法是选两个不同时间点的快照做 diff。比如线上服务启动 10 分钟后取一个基准快照运行 2 小时后再取一个快照然后用 pprof 对比增长方向go tool pprof -diff_baseheap.0001.heap -top ./your_service heap.0002.heap这样看的是第二个快照相对第一个快照的增量。如果某个调用栈的“存活空间”随时间线性增长而且增长量与业务波动不大相符那基本可以锁定为泄漏点。再配合调用栈里的函数名回代码里查是否有只分配不释放的对象即可。比如我抓过一个网络代理服务单看快照占比最高的都是连接池对象但 diff 之后发现增长最快的是一个 metrics 统计用的字符串列表——每个请求都往里 append 一条记录却没有清理逻辑。这类问题用 Valgrind 在压测环境里也能复现但用 Heap Profiler 直接在预发环境观察增长定位效率高得多。4.4 什么时候用 Heap Profiler 而不是 Valgrind我认为这两者的选择不应该是对立关系而是前后关系当服务规模较大、无法接受 Valgrind 的性能损耗时先上 Heap Profiler。当内存上涨来自多模块协作、不确定性高时用 Heap Profiler 先做大数据量上的“方向扫描”。当已经用 Heap Profiler 缩小到某个模块甚至某个函数时可以针对性地用 Valgrind 在测试环境尽量复现获得更严肃的证据。需要注意Heap Profiler 本质上替换了内存分配器程序的分配行为和 glibc malloc 下的表现会有差异。某些程序在 tcmalloc 下可能不泄漏、在 ptmalloc 下泄漏反之也有。所以它给出的结论要结合分配器差异来解释不能直接当成“程序实际运行状态”的定论。5. 三个工具的横向对比表与选型决策路径5.1 核心参数对比对比维度perfValgrind (Memcheck)Heap Profiler (gperftools)工作层级内核采样动态二进制插桩malloc 替换是否改变程序执行基本不改变改变极大慢 20-50 倍改变分配器行为能否在线使用可以几乎不行短时、预发场景可用泄漏判定能力不能直接判定可以直接判定 definite lost通过快照增长间接定位典型输出事件采样 调用栈精确泄漏报告堆快照 pprof 增量分析对进程内部原理依赖依赖内核事件不依赖内核、依赖 CPU 架构依赖符号和动态链接节奏盲区内部状态不可见系统调用 mmap/驱动映射难追踪非 malloc 分配、全局对象难覆盖这个表格做出来之后你会发现三者的关系不是替代关系而是从外到内、从粗到细、从线上到离线的层层递进。5.2 从报警到收敛的实战流程我实际带队排查内存泄漏时基本按下面这条路走每一步都有明确产出第一步先用 /proc 或监控系统确认内存增长的形态判断是 RSS 还是 VmSize区分堆、缓存、mmap 哪个在涨。第二步如果疑似堆增长用 perf 抓缺页事件结合进程内线程栈缩小到具体模块。这一步的核心产出是“战场地图”。第三步针对地图上的那些函数如果服务允许短时挂预发用 Heap Profiler 开一两个小时的快照对比增长找出增长最快的调用栈。第四步拿着这个调用栈最近代码里回查或者构造复现用例用 Valgrind 在测试环境下验证拿到 definitely lost 级别的结论。这套流程里perf 是侦察兵Heap Profiler 是数据分析师Valgrind 是检察官。只靠任何单点工具都容易出现“查了半天没有结论”的尴尬。5.3 常见误判与现实经验最后分享几个我踩过、也看别人反复踩的坑第一Valgrind 报告“干净”不代表内存真的没问题。它看不到进程外的映射、系统分配、以及部分现代指令集的模拟盲区。尤其多进程服务漏了子进程就等于漏了主要泄漏源。第二perf 抓到的 page-fault 栈只是“分配发生地”不代表“泄漏发生地”。有些分配是正常流量问题在于某处没有回收这时候要配合快照对比不能盯着单个函数下结论。第三Heap Profiler 和 Valgrind 结论不一致时先别急着怀疑工具坏了。它们原理不同盲区不同一个说增长另一个说没泄漏很多时候是因为问题的形态不在另一者的检测范围内。这时候回看进程的 VmData/RSS 增长来源往往能找到第三个答案比如缓存碎片或 mmap 映射。第四调优或排查内存泄漏时尽量在相同优化等级下比较结果。release 编译的指针生命周期和 debug 模式有明显差异用 debug 版跑 Valgrind 和用 release 版跑 Heap Profiler根因可能完全不是一回事。我个人在实际操作中的体会是内存泄漏排查很少是靠某一个“神器”一锤定音的它更像一个取证过程。perf、Valgrind、Heap Profiler 各有各的观测维度和成本曲线把三条证据链搓在一起结论才能让人信服。另外一个一直留存在心的小经验是Heap Profiler 的快照对比别只在内存涨到很高的阈值后再取应该在刚启动和上涨初期多留几份很多重要线索藏在增长曲线变陡的那个拐点附近。真等到报警时再开工具往往已经错过最容易定位的窗口期了。