1、发现问题起因
最近在ubuntu上开发了一个AI应用程序(用c++写的),通过循环拷机测试发现程序占用内存会不断往上涨(用htop监测虚拟内存VIRT和物理内存RES),虚拟内存可以从12G一直涨到30G,物理内存可以从起初的3G+涨到12G,怀疑内部存在严重的内存泄漏。
中间尝试了很多办法定位泄漏的地方,都莫名其妙的定位失败,包括利用valgrind工具分析、内部模块单个测试、注销某个单一模块测试等。
最终询问大模型才知这是内存假泄漏的现象。
2、什么是内存假泄漏?
虚拟内存或物理内存看起来在增长/波动,但实际上并没有发生代码级内存泄露。假泄漏的原因主要有以下几个因素:
2.1 多线程的 Arena 动态分配(ptmalloc 限制机制)-我的假泄漏就主要是这个原因
如果你的 C++ 程序使用了多线程(例如 std::thread、OpenMP 或第三方线程池),Glibc 的内存分配器(ptmalloc)会为每个线程或核心创建独立的内存分配区域,称为 Arena。
-
现象 :在 64 位系统下,ptmalloc 每次为一个新的 Arena 初始化的虚拟内存默认高达 64MB。
-
影响:线程创建的先后顺序、线程间的竞争激烈程度、以及内核调度的随机性,会导致每次运行中被激活的 Arena 数量不同。如果这次测试激活了 2 个 Arena,下次激活了 8 个,光是这部分就会导致虚拟内存暴增数百兆。
2.2 内存碎片化
程序频繁申请和释放不同大小的内存块,导致动态内存(堆)中充满了大量不连续的、微小的空闲孔洞
- 现象:当程序后续需要申请一个较大块的内存时,虽然空闲孔洞的总和足够,但由于物理地址不连续,内存分配器(ptmalloc)无法使用这些孔洞,被迫再次向内核申请新的内存。这导致程序虽然释放了对象,但内存指标却一直在涨,形成"假泄露"。
2.3 内存留存/缓存池化(Memory Retaining / Caching)
为了避免频繁向内核发起系统调用(如 brk、mmap)带来的性能损耗,内存分配器在程序执行 free 或 delete 后,并不会立刻把内存还给操作系统 ,而是保留在自己的私有缓冲池(如 Arena、Thread Cache)中,留给接下来的 new / malloc 复用。
现象:从操作系统和任务管理器的视角来看,该进程的物理内存(RSS)或虚拟内存(VIRT)到达峰值后就再也没有降下来,看起来就像是泄露了。
2.4 ASLR(地址空间布局随机化)
Linux 内核为了防止缓冲区溢出攻击,默认开启了 ASLR
-
现象:每次启动程序时,内核会随机初始化进程的堆、栈、共享库映射区(mmaps)的起始地址。
-
影响:为了在随机化后腾出足够连续的地址空间,或者因为对齐(Alignment)要求,内核在分配内存时会产生随机的地址空隙(Gaps)。这些空隙计入虚拟内存,导致每次运行的虚拟内存总额不同。
3、排查是否是假泄漏
第一步:临时关闭 ASLR 进行对比测试
sudo sysctl -w kernel.randomize_va_space=0
-
如果关闭后虚拟内存峰值完全稳定 :说明波动纯粹是由 Linux 安全机制(ASLR)引起的,无需优化代码,因为这不影响真实的物理内存。
-
测试完后请恢复默认开启状态 :
sudo sysctl -w kernel.randomize_va_space=2。
第二步:限制多线程 Arena 数量
如果你的程序是多线程的,可以在启动程序前,通过环境变量限制 Glibc 允许创建的 Arena 最大数量:
export MALLOC_ARENA_MAX=2
./your_cpp_program
- 限制后如果虚拟内存峰值不再剧烈波动,说明是多线程内存分配器的 Arena 导致的。
- 提示:将
MALLOC_ARENA_MAX设低可以有效压减虚拟内存和物理内存碎片,但可能会轻微降低极高并发下的内存分配性能。
4、解决办法
直接想到的就是限制多线程 Arena 数量,但是这会带来多并发下的性能损失,建议使用jemalloc代替glibc的方案:
零代码修改:
# 安装 jemalloc (Ubuntu)
sudo apt-get install libjemalloc-dev
# 在启动程序前通过 LD_PRELOAD 强制加载
export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so
./your_program
静态/动态编译链接:
find_package(PkgConfig REQUIRED)
pkg_check_modules(JEMALLOC REQUIRED jemalloc)
add_executable(your_cpp_program main.cpp)
target_link_libraries(your_cpp_program PRIVATE ${JEMALLOC_LIBRARIES})
我的采用jemalloc之后虚拟内存峰值可以从31G减少到12G,物理内存可以从12G减少到2770M,耗时上基本上没有变化。
5、jemalloc vs glibc
5.1 核心架构与机制对比

5.2 性能与资源消耗对比(C++ 场景)
- 运行时耗时(Latency & QPS)
- glibc: 在单线程下性能很好。但在多线程高并发下,因为锁竞争剧烈,耗时会发生无规律的"毛刺"(长尾延迟)。通过
MALLOC_ARENA_MAX=4限制它,更是雪上加霜。 jemalloc: 高并发下耗时曲线非常平滑,QPS 吞吐量通常能提升 15% ~ 30%。
- 物理内存占用(RSS)
- glibc: 长时间运行的 C++ 守护进程,物理内存通常会一路上扬("假泄漏"),业务低谷期内存也完全不下降。
jemalloc: 内存呈"波浪形"。业务高峰期上涨,业务低谷期物理内存会真正回落。
- 虚拟内存占用(VIRT)
- glibc: 默认行为下,因为 64MB Arena 的机制,多线程程序的虚拟内存动辄高达几十 GB。
jemalloc: 虽然也使用mmap,但由于没有 64MB 固定块的限制,虚拟内存的膨胀速度远慢于 glibc。