在ubuntu20.04下由arena引发的内存”泄露“
- 注:标题中的”泄露“不是真正的内存未被用户释放的泄露;而是在程序用用户正确的释放堆中的内存,长期运行时,内存在不停的增长丝毫没有降低的趋势。
近期,在linux环境中开发硬件固件时出现程序运行时内存在不停的增长,而上一个版本被没有此问题。使用valgrind测试发现又没有内存泄露问题!-……-难搞啊。
大胆猜测!
是不是内存释放导致的,使用jemalloc替换系统自带的malloc测试发现,内存完全没有泄露的迹象。
jemalloc使用方式
wget https://github.com/jemalloc/jemalloc/archive/refs/tags/5.3.0.tar.gz
tar zxvf 5.3.0.tar.gz
cd jemalloc-5.3.0/
./autogen.sh
# 这里启动堆剖析功能
./configure --enable-prof
make -j
make install
使用jemalloc需要两个步骤
1.在debug时,需要进行采样配置MALLOC_CONF
# ""配置选项皆可以在https://jemalloc.net/jemalloc.3.html#opt.prof_leak找到
export MALLOC_CONF="prof_leak:true,lg_prof_sample:20,prof_final:true"
- prof_leak,用来分析内存泄露。
- lg_prof_sample:20,采样内存的平均间隔(以 2 为底的对数),以分配活动字节数为单位。这里设置为 20,即每申请 2^20 字节的内存就会采样一次。默认情况下是 2^19(512KB)。
- prof_final: true,在程序退出的时候生成一次 dump。
2.编辑测试程序并运行程序
// testleak.cc
#include <stdio.h>
#include <stdlib.h>
void do_something(size_t i)
{
// Leak some memory.
char* buffer = (char*)malloc(i * 1024);
}
void do_something_else(size_t i)
{
// Leak some memory.
char* buffer = (char*)malloc(i * 4096);
char* buf1 = new char[i*4096];
}
int main(int argc, char **argv)
{
size_t i, sz;
for (i = 0; i < 80; i++)
{
do_something(i);
}
for (i = 0; i < 40; i++)
{
do_something_else(i);
}
return (0);
}
# 编译
g++ testleak.cc -o testleak -ljemalloc
# 使用LD_PRELOAD可以使执行文件连接到jemalloc上
LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" ./testleak
执行完成后会生成该程序对应的jeprof.xxx.heap文件
使用jeprof分析或生成pdf文件进行查看
$ jeprof testleak jeprof.1384301.0.f.heap
Using local file testleak.
Using local file jeprof.1384301.0.f.heap.
Welcome to jeprof! For help, type 'help'.
(jeprof) top
Total: 2.1 MB
2.1 100.0% 100.0% 2.1 100.0% prof_backtrace_impl
0.0 0.0% 100.0% 1.0 49.0% GLIBC_2.17
0.0 0.0% 100.0% 1.1 51.0% __libc_start_main
0.0 0.0% 100.0% 1.0 49.0% _dl_rtld_di_serinfo
0.0 0.0% 100.0% 1.1 51.0% _start
0.0 0.0% 100.0% 1.1 51.0% do_something_else
0.0 0.0% 100.0% 2.1 100.0% imalloc (inline)
0.0 0.0% 100.0% 2.1 100.0% imalloc_body (inline)
0.0 0.0% 100.0% 2.1 100.0% je_malloc_default
0.0 0.0% 100.0% 2.1 100.0% je_prof_backtrace
# 解析生成pdf文件
$ jeprof --show_bytes --pdf testleak jeprof.1384301.0.f.heap > teskleak.pdf
问题根因及解决思路
怀疑:在使用完jemalloc进行分析后发现,程序内存泄露消失了,这是开始怀疑是否真的是glibc中malloc/free没有正确的释放内存给操作系统,导致内存”泄露“问题。
查找glibc malloc内存管理机制,发现是由arena模块进行内存管理。
Arena 设计初衷
- Arena 机制的目的是减少多线程的锁竞争,每个线程可拥有独立的 arena。
- 每个 arena 自己维护一个空闲内存池,即使内存没有被程序使用,glibc 也不一定会将内存归还操作系统,而是继续保留这些内存,以便后续分配。
使用如下测试代码块加入到程序中
#include <malloc.h>
#include <stdio.h>
{
malloc_stats(); // 输出 malloc 的基本统计信息
}
发现,该程序分配了64个arena进行管理内存,与我们知晓的CPU核心数*4完全不相符,我们核心数为8,理应最多分配32个Arena进行管理,并且在每次运行malloc_stats后,in use bytes并没有增多,而system bytes在不停的增加;这就说明了glibc malloc管理的内存池中的空闲字节在不断的增加并没有释放给系统,导致系统内存出现”泄露“现象。
解决方案
方法 1: 调整 MALLOC_ARENA_MAX
MALLOC_ARENA_MAX
控制最大 arena 数量。
-
默认值:
min(8, 2 * number_of_cores)
,多核服务器默认会生成多个 arena。 -
调整方法
:
bash复制代码export MALLOC_ARENA_MAX=1 ./your_program
推荐值
- 对于小内存占用程序:
MALLOC_ARENA_MAX=1
- 对于内存敏感型程序:
MALLOC_ARENA_MAX=2
- 对于高性能并发程序:
MALLOC_ARENA_MAX=8
(默认值)
⚠️ 风险提示:如果
MALLOC_ARENA_MAX
过小,多线程的内存分配性能会下降,因为多个线程需要竞争同一个 arena 锁。
方法 2: 使用 malloc_trim()
主动释放内存
malloc_trim()
函数是 glibc 的内存回收接口,它会将不使用的内存归还操作系统。
代码示例
#include <malloc.h>
#include <stdio.h>
int main() {
void *ptr = malloc(1024 * 1024 * 100); // 100MB
free(ptr); // 释放内存,但未归还操作系统
malloc_trim(0); // 主动归还内存
printf("Memory returned to system.\n");
return 0;
}
malloc_trim(0)
会将当前进程中空闲的堆内存归还操作系统,但对使用mmap 分配的大对象不生效。
常见的调用位置
- 在程序的空闲时间调用
malloc_trim(0)
,以释放内存。 - 例如,定时器中每隔 30 秒调用一次
malloc_trim(0)
,用于回收空闲内存。
方法 3: 使用 tcmalloc
或 jemalloc
替代 glibc malloc
- glibc malloc 的 arena 机制适用于一般场景,但在高并发或长时间运行的服务器中,容易产生碎片。
- jemalloc 和 tcmalloc 可以提供更高效的内存管理,并减少空闲内存的浪费。
使用 jemalloc
LD_PRELOAD=/usr/lib/libjemalloc.so.2 ./your_program
使用 tcmalloc
LD_PRELOAD=/usr/lib/libtcmalloc.so ./your_program