Linux 内存泄漏排查与修复实践

1次阅读

valgrind定位泄漏需带调试符号编译(gcc -g -o0),关注含文件名和行号的分配点;它仅跟踪malloc/new等,不监控mmap/brk;长时服务宜用gdb动态断点或/proc/pid/smaps分析pss。

Linux 内存泄漏排查与修复实践

valgrind 抓住堆内存泄漏的准确位置

多数人跑 valgrind --leak-check=full ./a.out 后只扫一眼“definitely lost”就停了,但真正有用的是它输出中带文件名和行号的那一行——前提是你的程序得带调试符号编译。

  • 必须用 gcc -g -O0 编译,-O2 会让内联和变量优化掩盖真实分配点
  • valgrindmmapbrk 级别分配不敏感,只管 malloc/calloc/realloc 系列,C++ 的 new 也算在内
  • 如果程序一启动就崩溃,加 --tool=memcheck --track-origins=yes 查未初始化内存是否间接导致后续误释放
  • 注意 suppressions 文件干扰:默认 suppressions 可能过滤掉 glibc 内部的假阳性,但自定义 suppressions 写错会漏报真泄漏

gdb 里动态观察 malloc 调用

不是所有泄漏都适合等程序退出再查;长时运行服务(比如后台 daemon)需要在线盯住谁在不断 malloc 却不 free

  • gdb -p $(pidof your_service),然后 catch syscall mmapbreak malloc,再 command 1; bt; cont; end 让每次分配都打个断点栈
  • 更轻量的做法是 watch *(int*)0xdeadbeef —— 先用 cat /proc/PID/maps 找到堆地址范围,再对某块堆内存设写入观察点,触发时看谁在改它
  • 注意 glibcmalloc 实现有 fastbin、tcache,小对象可能复用不走系统调用,这时候 catch syscall brk 就捕不到,得回退到 break __libc_malloc

/proc/PID/smaps 看懂 RSS 和 PSS 差异

运维常盯着 top%MEMps auxVSZ 判断泄漏,但这些数字根本不能定位问题——VSZ 包含没实际映射的虚拟地址,RSS 又包含共享库和 mmap 共享内存。

  • 真正反映进程独占物理内存的是 PSS(Proportional Set Size),在 /proc/PID/smaps 每个内存段后都有,总和才接近真实增长量
  • 重点看 AnonHugePagesMMUPageSize 字段:如果某段 Size 很大但 RSS 接近 0,说明只是预留了虚拟地址,还没真正分配物理页
  • awk '/^Size:/ {s+=$2} /^PSS:/ {p+=$2} END {print "Size:", s, "PSS:", p}' /proc/PID/smaps 快速汇总,比单看 top 可靠得多

C++ 中 std::shared_ptr 循环引用的真实表现

不是所有 C++ 内存泄漏都报错或 crash,shared_ptr 循环引用会导致对象永远不析构,但 valgrind 也标为 “still reachable”,容易被当成正常缓存。

  • 典型模式:A 持有 shared_ptr<b></b>B 又持有 shared_ptr<a></a>;用 weak_ptr 替换其中一端才能打破循环
  • valgrind 输出里如果看到大量 “still reachable” 且堆栈指向 std::shared_ptr<...>::_M_release</...>,基本就是这个原因
  • 不要依赖 std::enable_shared_from_this 自动管理,它本身不解决循环,反而可能让引用关系更隐蔽

堆内存泄漏最难的不是发现,是确认那个 malloc 调用到底该由谁 free——尤其是跨模块、跨线程、或者封装在第三方库回调里的分配。这时候看调用栈比看代码行号还重要。

text=ZqhQzanResources