ubuntu:内存假泄漏

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)

为了避免频繁向内核发起系统调用(如 brkmmap)带来的性能损耗,内存分配器在程序执行 freedelete 后,并不会立刻把内存还给操作系统 ,而是保留在自己的私有缓冲池(如 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++ 场景)

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

相关推荐
逸模12 小时前
告别熬夜手工整理台账,逸模智能归集实现项目数据自动化存档
大数据·运维·人工智能·笔记·其他·信息可视化·自动化
sbjdhjd12 小时前
Redis 主从复制、哨兵高可用与 Cluster 集群部署实验手册
运维·前端·redis·云原生·开源·bootstrap·html
人间乄惊鸿客12 小时前
Linux所遇问题自记录
linux
z落落12 小时前
C# 泛型方法(原理、类型推断、多泛型参数)+泛型效率(普通类型 VS Object装箱 VS 泛型)
开发语言·c#
L_090712 小时前
【C++】异常
开发语言·c++
liulilittle12 小时前
关于拥塞控制的几点思考
网络·c++·tcp/ip·计算机网络·信息与通信·tcp·通信
weixin_3975740912 小时前
生产管理和设备管理:制造执行层的AI痛点
人工智能·制造
冬奇Lab12 小时前
Agent 系列(16):工具链设计——让 LLM 用对工具的五个原则
人工智能·llm·agent
冬奇Lab12 小时前
每日一个开源项目(第125篇):taste-skill - 给 AI 装上审美,让前端不再千篇一律
人工智能·开源·agent
AOwhisky12 小时前
MySQL 学习笔记(第四期):SQL 语言之多表查询
linux·运维·网络·数据库·笔记·学习·mysql