凌晨三点,监控告警突然响起——某台线上服务器的内存使用率在短短两小时内从 40% 飙升到 98%,随后 SSH 连接超时,业务进程被系统强制杀死。登录带外管理口查看 dmesg,一行熟悉的日志映入眼帘:
Out of memory: Kill process 12345 (myapp) score 892 or sacrifice child
Killed process 12345 (myapp) total-vm:8192000kB, anon-rss:7864320kB
这是典型的 OOM Killer 出手了。但问题在于:为什么 myapp 会吃掉 7.8GB 内存? 是配置不当、突发流量,还是真正的内存泄漏?本文将从 OOM Killer 日志出发,一步步还原一次真实的内存泄漏排查过程,最终用 valgrind 精准定位到问题代码。
一、读懂 OOM Killer 日志:线索藏在细节里
很多人看到 OOM 日志只关注“哪个进程被杀”,但真正有价值的信息在后面的数字:
total-vm:8192000kB:进程虚拟内存总量约 8GBanon-rss:7864320kB:匿名页常驻内存约 7.8GB,这是堆内存的直接体现score 892:OOM 评分,越高越容易被杀
关键判断:anon-rss 持续增长且不回落,基本可以排除“缓存占用”或“共享库映射”的干扰,指向堆内存泄漏。如果只是流量高峰导致的短时内存上涨,OOM 后重启进程内存会恢复正常;而泄漏的特征是——重启后内存曲线依然呈阶梯式上升。
二、现场保护与初步排查
OOM 发生后,切忌急着重启了事。先做三件事:
1. 检查系统级内存分布
free -m
cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|Slab|SReclaimable"
如果 Slab 异常大,可能是内核对象泄漏;如果 SReclaimable 很小而 MemFree 极低,则确认是用户态进程吃掉了内存。
2. 查看进程内存映射
如果进程还活着(或 core dump 还在),用 pmap 观察堆区:
pmap -x <pid> | sort -k3 -n -r | head -20
重点关注 [heap] 段和大量匿名映射([anon])。如果 [heap] 持续增长,说明 malloc/new 分配的内存没有释放。
3. 确认泄漏速率
while true; do
ps -o rss= -p <pid> >> /tmp/rss.log
sleep 60
done
观察 RSS 是否线性增长。如果每小时稳定增长 200MB,那基本可以坐实泄漏。
三、用 valgrind 精准定位泄漏点
确认存在泄漏后,下一步是找到哪一行代码在泄漏。valgrind 的 memcheck 工具是最经典的选择。
1. 编译带调试信息的版本
gcc -g -O0 -o myapp myapp.c
注意:-O0 避免优化干扰行号,-g 保留符号表。
2. 运行 valgrind
valgrind --leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
--log-file=valgrind.log \
./myapp
参数说明:
--leak-check=full:显示每个泄漏点的完整调用栈--show-leak-kinds=all:包括 definitely/indirectly/possibly lost--track-origins=yes:追踪未初始化值的来源,对定位有帮助
3. 解读输出
valgrind 报告通常长这样:
==12345== 4,096 bytes in 1 blocks are definitely lost in loss record 12 of 20
==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck.so)
==12345== by 0x400B2A: create_buffer (buffer.c:45)
==12345== by 0x400C1F: process_request (handler.c:88)
==12345== by 0x400D33: main (main.c:22)
关键看 definitely lost——这是确定泄漏。调用栈直接指向 buffer.c:45 的 malloc,而 handler.c:88 调用了它却没有对应的 free。
四、生产环境无法跑 valgrind 怎么办?
valgrind 会带来 10~50 倍性能损耗,线上服务不可能直接跑。替代方案:
方案一:使用 AddressSanitizer(ASan)
编译时加 -fsanitize=address -fno-omit-frame-pointer,运行时开销约 2 倍,适合预发环境。泄漏报告格式与 valgrind 类似,但更快。
方案二:tcmalloc/jemalloc 的堆分析
如果程序链接了 tcmalloc,可以通过环境变量开启堆采样:
HEAPPROFILE=/tmp/heap ./myapp
然后用 pprof 分析堆快照,对比两个时间点的增长,直接定位分配热点。
方案三:gdb 附加 + malloc 钩子
对于已经出问题的进程,可以用 gdb attach 后调用 malloc_stats() 查看分配统计,或者用 mtrace 记录分配轨迹。
五、常见泄漏模式与修复
根据经验,Linux C/C++ 程序的内存泄漏集中在几类:
- 异常路径未释放:
try中new,catch中直接return忘了delete。修复:用 RAII 或智能指针。 - 容器只增不减:
std::vector或std::map持续push_back/insert,从不清理。修复:加定期清理逻辑或改用 LRU 缓存。 - 回调注册未注销:对象销毁了,但回调仍被全局管理器持有。修复:析构时反注册。
- 循环引用:
shared_ptr互相持有。修复:一方改用weak_ptr。
六、总结
内存泄漏排查的完整链路是:
- OOM 日志确认 anon-rss 异常增长
- pmap / ps 确认泄漏速率和堆区增长
- valgrind / ASan / tcmalloc 定位到具体代码行
- 修复 + 回归测试,并用监控验证内存曲线是否平稳
记住一个原则:先保护现场,再复现问题,最后才动代码。OOM Killer 杀掉的不仅是进程,还可能抹掉唯一的线索。养成保留 core dump 和日志的习惯,排查效率会提升数倍。
未经允许不得转载:任鹏个人博客 » Linux 内存泄漏排查实战:从 OOM Killer 日志到 valgrind 定位


朋友圈点赞图在线生成源码