不是所有内存上涨都是泄漏;缓存未释放与真正泄漏现象相似但根因不同:前者内存仍被强引用(如全局缓存容器),后者分配后完全不可达;实操中应通过pprof分析分配栈、检查析构函数执行、区分capacity与实际释放来精准定界。

不是所有内存上涨都是泄漏;缓存未释放和真正的泄漏在现象上几乎一样,但根因、排查路径和修复方式完全不同。
看 RSS 增长是否伴随请求量/业务负载线性相关
如果进程 RSS 每小时涨 20MB,且这段时间内 QPS 稳定在 500,而你改用压测工具把 QPS 降到 50 后,RSS 增速也同步降到 1/10(约 2MB/h),那大概率不是传统泄漏,而是跟请求生命周期强绑定的资源堆积——比如缓存、队列、连接池未设上限或未淘汰。
常见误判点:
- 误把
std::vector::clear()当成释放内存:它只清空元素,不归还堆空间,capacity()不变 - 误以为
std::map::erase()后内存立刻下降:红黑树节点释放有延迟,且碎片化严重时系统未必立即返还给 OS - 用
top看%VSZ上涨就断定泄漏:VSZ包含 mmap 区域、保留地址空间等,不能反映真实堆使用
用 malloc 跟踪工具区分“新分配未释放”和“已分配但被持有”
真正泄漏的特征是:内存块分配后,没有任何活跃指针指向它(即“不可达”);而缓存未释放的特征是:内存块仍被某个全局容器、单例管理器、回调注册表等强引用着,工具会显示“可达但长期不析构”。
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
实操建议:
- 用
libtcmalloc的heap profiler(比 Valgrind 轻量):运行pprof --text ./binary heap_profile,重点看 top 函数中是否大量出现new、malloc,且调用栈集中在某几个类的构造或缓存写入逻辑 - 若发现高占比分配都来自
CacheManager::Put()或ConnectionPool::Acquire(),但没看到对应Evict()或Release()调用,则是缓存策略问题,不是泄漏 - 若调用栈里频繁出现
std::shared_ptr<T>::shared_ptr且伴随循环引用典型模式(如 A 持有 B 的shared_ptr,B 又持有 A 的weak_ptr以外的任何强引用),就是生命周期泄漏,不是“忘了 free”
检查析构函数是否执行,这是最直接的分水岭
在疑似对象的析构函数第一行加日志或断点,例如:
~MyCacheItem() {
LOG(INFO) << "MyCacheItem destroyed, addr=" << this;
}
然后观察:
- 如果压测期间完全没打印这条日志,说明对象根本没销毁 → 很可能是被某处长期持有(如全局
std::unordered_map<int, std::shared_ptr<MyCacheItem>>未清理) - 如果日志大量出现,但 RSS 还在涨 → 说明对象释放了,但释放的内存没被系统回收(如 tcmalloc 的 page cache 机制,或 mmap 分配的大块内存未
madvise(MADV_DONTNEED)) - 如果日志只在进程退出时集中打印 → 极大概率是单例、静态容器或线程局部存储(TLS)导致的生命周期失控
真正难的不是“找哪行 new 没配对 delete”,而是确认一个对象“该不该活这么久”。很多所谓“内存泄漏”,其实是设计时没想清楚谁负责释放、什么时候释放、释放后要不要通知其他模块。一旦开始怀疑是缓存问题,就别再盯着 malloc 调用次数,转而去查容量控制、淘汰策略、shutdown 流程是否完整执行。

















