服务的 RSS 从 500 MiB 涨到 2 GiB,不足以证明发生了内存泄漏。它可能有不可达的堆对象,也可能只是缓存仍被业务持有、文件映射变多、线程栈增加、共享页计入方式变化,或分配器保留了空闲 arena 没有归还内核。第一步不是换工具,而是先给“增长”分类。
三层问题不要混在一起
应用层关心对象是否还可达、缓存是否有上限;分配器层关心 arena、碎片和线程缓存;内核层看到的是匿名页、文件页、共享页和 swap。三个层次的数字不会天然相等。
先连续采样趋势,不要只保存一次 top 截图:
1 | PID=1234 |
如果 RssAnon 持续增长,重点看堆、匿名 mmap、线程栈和 allocator;RssFile 增长更可能与文件映射、共享库或 page cache 映射有关;RssShmem 则应追踪 tmpfs、共享内存与相关进程。实际分类以 smaps 的每个 VMA 为准。
泄漏检测器能回答什么
能在测试环境稳定复现时,LeakSanitizer 通常是第一选择。它在进程退出时检查无法从根集合到达的分配:
1 | clang -g -O1 -fno-omit-frame-pointer -fsanitize=address,leak demo.c -o demo |
LSan 报告“direct leak”并不自动指出业务修复方式。需要沿分配栈找所有权为何丢失。相反,缓存表里仍有指针的对象在可达性上不是 leak,哪怕缓存没有淘汰策略、最终吃光内存。此时要采样对象数量、键空间和缓存命中,找到谁还在持有。
Valgrind 不要求插桩构建,但运行开销高,线程时序和性能可能与线上明显不同。两种工具都更适合可控输入,不能不加评估直接挂在生产进程上。
分配器滞留与碎片
对象已经 free(),RSS 仍可能不降。glibc 等分配器会把空闲块留在用户空间,供后续分配复用;多线程 arena、大小类别和不连续空洞也可能让部分页无法及时归还。这不一定是泄漏,但会形成真实的内存容量压力。
判断方法是同时观察业务对象数、累计分配量和 RSS。若活跃对象稳定、RSS 在预热后达到平台,可能是缓存或 allocator 稳态;若活跃对象持续增长,应追踪所有权;若对象下降但 RSS 长期保持高位,则进一步分析碎片、arena 和 mmap 分布。
1 | # 按 Private_Dirty 排出 smaps 中值得继续看的映射需要额外解析 |
一套可复核的结论
修复报告至少要留下:输入负载、观察时长、版本、峰值与平台 RSS、关键映射变化、分配调用栈、活跃对象计数,以及修复前后同条件对比。只写“运行一晚没有再涨”很难排除负载不足或采样遗漏。
还要检查 OOM 日志和 cgroup 限制。进程可能没有传统泄漏,却因容器 memory.max 太小、共享页计费或突发缓存而被杀。系统内存充足也不能说明某个 cgroup 没有达到上限。
证据边界
本文面向用户态进程。内核 slab、页表、驱动 DMA 缓冲和 page cache 的系统级增长需要其他指标。smaps 是采样时刻的映射统计,读取本身也有成本;LSan 与 Valgrind 只覆盖它们能追踪到的分配和执行路径。任何“已修复”结论都应在相同版本、负载、时长与内存限制下复测。
参考:proc_pid_smaps(5) · LeakSanitizer · Valgrind Memcheck Manual · Linux 如何查找内存泄漏和内存占用过大?