jemalloc分析内存

342 阅读9分钟

 分析内存泄漏过程中, 由于tcmalloc不能长时间开启heap profile(会不停涨内存,导致内存爆掉).尝试换jemalloc.

编译选项:jemalloc/INSTALL.md at dev · jemalloc/jemalloc · GitHub

--enable-debug -enable-fill :调试选项,程序项目中不应该开启

交叉编译:

 git clone https://github.com/jemalloc/jemalloc.git

 ./autogen.sh

 ./configure --host=aarch64-none-linux-gnu --prefix=/code/jemalloc/install --enable-prof

make -j4

make isntall

初始化

当第一次调用内存分配例程时,分配器会根据编译或运行时指定的各种选项初始化其内部结构。

  • 通过 --with-malloc-conf 指定的字符串

    • --with-malloc-conf="prof:true,prof_prefix:/tmp/jemalloc_dumps,background_thread:true"
      
  • 全局变量 malloc_conf 指向的字符串

    • const char* malloc_conf="prof:true,prof_prefix:/tmp/jemalloc_dumps,background_thread:true"
      
  • 由名为 /etc/malloc.conf 的符号链接引用的文件 "name"

  • 以及环境变量 MALLOC_CONF 的值,将按从左到右的顺序解释为选项。MALLOC_CONF 会覆盖 malloc_conf

    • export MALLOC_CONF="prof:true,prof_prefix:jeprof.out"
      
  • 请注意,malloc_conf 可能会在进入 main() 之前被读取,因此在声明 malloc_conf 时应指定一个初始化器,其中包含 jemalloc 将读取的最终值。 --with-malloc-conf和malloc_conf是编译时机制,而/etc/malloc.conf和MALLOC_CONF则可以在调用程序之前随时安全地设置。

选项字符串是以逗号分隔的选项:值对列表。 mallctl 的每个选项都有一个键(参见MALLCTL NAMESPACE 部分的选项文档)。 例如,abort:true,narenas:1 设置 opt.abort 和 opt.narenas 选项。 有些选项有布尔值(true/false),有些有整数值(基数为 8、10 或 16,取决于前缀),还有些有原始字符串值。

参数设置

int mallctl(	const char *name,
 	void *oldp,
 	size_t *oldlenp,
 	void *newp,
 	size_t newlen);

读写属性,可读,可设置
prof.active (bool) rw [--enable-prof]

只读属性,只能读不能设置
opt.prof (bool) r- [--enable-prof]

只写属性,不能读
prof.prefix (const char *) -w [--enable-prof]
size_t sz = sizeof(bool);
auto ret = mallctl("opt.prof", NULL, 0, &prof_enabled, sz);//错误,返回1,只能读不能写
bool result = false;
ret = mallctl("opt.prof", &result, sizeof(bool), NULL,0);//崩溃,错误,sizeof(bool)临时变量,需传入地址
ret = mallctl("opt.prof", &result, &sz, NULL,0);//正确,返回0

const char *prof_prefix = "/tmp/myapp_profile";
size_t prefix_len = strlen(prof_prefix);//错误
size_t prefix_len = sizeof(prof_prefix);//实际校验的是指针大小,可能源码有问题
ret = mallctl("prof.prefix", NULL, 0, prof_prefix , prefix_len); //错误,需要传入&prof_prefix
ret = mallctl("prof.prefix", NULL, 0, &prof_prefix , prefix_len);//正确,注意不支持读,读如果结果传入为空,则会停止生成dump文件

优化

background_thread
启用 jemalloc 后台线程通常能改善应用线程的尾部延迟,因为未使用的内存清理工作被转移到了专用的后台线程上。 此外,使用后台线程还能避免因应用程序不活动而造成的意外清理延迟。
建议:当允许使用 jemalloc 托管线程时,background_thread:true

metadata_thp:
允许 jemalloc 为其内部元数据使用透明的超大页面,通常能显著减少 TLB 错失,尤其是对于内存占用较大、分配/取消分配活动频繁的程序。 元数据内存使用量可能会因使用超大页而增加。
建议分配密集型程序使用:metadata_thp:auto 或 metadata_thp:always,它们有望以较小的内存代价提高 CPU 利用率。
约束:

  • 检查 jemalloc 版本:mallctl("version", ...) 确认版本 ≥ 5.2.1。
  • 确认编译时启用 --enable-thp。
  • 检查内核 THP 状态:/sys/kernel/mm/transparent_hugepage/enabled。

缺点

  • 内存浪费:若 metadata_thp 启用后 RSS 内存显著增加,可尝试:

    • 改用 metadata_thp:auto。
    • 完全禁用:metadata_thp:never。
    • 调优建议:结合 perf 或 jemalloc 内存统计工具(如 stats.arenas..metadata_thp)评估实际收益。

dirty_decay_ms and muzzy_decay_ms
衰减时间决定了 jemalloc 将未使用页面返回操作系统的速度,因此可以在 CPU 和内存使用量之间进行相当直接的权衡。 较短的衰减时间能更快地清除未使用的页面,从而减少内存使用量(通常以花费更多 CPU 周期清除内存为代价),反之亦然。
建议:根据所需的权衡调整数值。

narenas
默认情况下,jemalloc 使用多个竞技场来减少内部锁竞争。 不过,由于竞技场是独立管理内存的,因此竞技场数量过多也会增加整体内存碎片。 如果不希望在分配器层面实现高度并行,减少竞技场数量往往能提高内存使用率。
建议:如果预期并行性较低,可尝试降低竞技场数量,同时监控 CPU 和内存使用情况。

percpu_arena
根据运行中的 CPU,启用线程与竞技场的动态关联。 这有可能改善局部性,例如当线程与 CPU 存在亲和性时。
建议:如果预计线程在处理器间迁移的频率不高,可尝试使用 percpu_arena:percpu 或 percpu_arena:phycpu。

例子:

  • 高资源消耗应用程序,优先使用 CPU:

    • background_thread:true,metadata_thp:auto,同时放宽衰减时间(增加 dirty_decay_ms 和/或 muzzy_decay_ms,例如 dirty_decay_ms:30000,muzzy_decay_ms:30000)。
  • 高资源消耗应用程序,优先使用内存:

    • background_thread:true,tcache_max:4096,同时缩短衰减时间(减少 dirty_decay_ms 和/或 muzzy_decay_ms,例如 dirty_decay_ms:5000,muzzy_decay_ms:5000),并降低竞技场数量(例如 CPU 数量)。
  • -低资源消耗应用:

    • narenas:1,tcache_max:1024,同时缩短衰减时间(减少 dirty_decay_ms 和/或 muzzy_decay_ms,例如 dirty_decay_ms:1000,muzzy_decay_ms:0)。
  • 极其保守 -- 不惜一切代价尽量减少内存使用量,仅适用于分配活动非常少的情况:

    • narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0
  • 注意,建议将这些选项与 abort_conf:true 结合使用,这样在出现非法选项时会立即终止。

堆剖析(Heap Profiling)

除了在退出时进行泄漏检查( leak checking at exit)外,jemalloc 还可以转储配置文件:

  • 大约每分配这么多字节。 参见 prof_leak and lg_prof_interval MALLOC_CONF 选项。

  • // 设置采样间隔为 1MB (2^20)
    void set_sampling_interval_1mb() {
        size_t lg_sample = 20;
        mallctl("prof.lg_sample", NULL, NULL, &lg_sample, sizeof(size_t));
    }
    
    // 启用内存增长自动转储
    void enable_auto_dump() {
        bool gdump = true;
        mallctl("prof.gdump", NULL, NULL, &gdump, sizeof(bool));
    }
    
  • 每次使用中的虚拟内存总量达到新高时。 参见 prof_gdump MALLOC_CONF 选项。

  • 通过  ["prof.dump"](jemalloc.net/jemalloc.3.… ""prof.dump"") mallctl 手动执行。 使用类似下面的函数调用(当然要进行适当的错误检查)。

  • 转储到指定文件名。

    • 在 MALLOC_CONF 中将 prof 设为 true:

    • export MALLOC_CONF="prof:true,prof_prefix:jeprof.out"
      
    • 然后执行与以下 C 代码类似的代码:

    • const char *fileName = "heap_info.out";
      mallctl("prof.dump", NULL, NULL, &fileName, sizeof(const char *));
      
  • 使用自动生成文件名。

    • 在 MALLOC_CONF 中设置 prof_prefix 和 prof:

    • export MALLOC_CONF="prof:true,prof_prefix:jeprof.out"
      
    • 然后执行与以下 C 代码类似的代码:(必须执行才会生成dump文件)

    • mallctl("prof.dump", NULL, NULL, NULL, 0);
      
    • 动态设置路径

    • const char *prof_prefix = "/tmp/myapp_profile";
      size_t prefix_len = strlen(prof_prefix);//错误
      size_t prefix_len = sizeof(prof_prefix);//实际校验的是指针大小,可能源码有问题
      ret = mallctl("prof.prefix", NULL, 0, prof_prefix , prefix_len); //错误,需要传入&prof_prefix
      ret = mallctl("prof.prefix", NULL, 0, &prof_prefix , prefix_len);//正确,注意不支持读,读如果结果传入为空,则会停止生成dump文件
      

        jeprof 可以比较产生的一系列配置文件转储中的任意两个,并显示在这段时间内发生了哪些分配活动。 使用 --base= 标志指定第一个配置文件。
通过指定 MALLOC_CONF=prof_active:false,可以在启用剖析但未激活的情况下启动应用程序。 这只有在应用程序执行期间通过 "prof.active" mallctl 手动激活/禁用剖析时才有用。 用例包括

  • 在初始化完成后激活剖析,使剖析只显示稳态执行期间分配的对象。
  • 转储一个配置文件,激活剖析 30 秒,停用剖析后等待 30 秒,然后转储另一个配置文件,并使用 jeprof 比较两个转储文件。 这将集中显示在稳态执行期间分配的对象,但这些对象是长期存在的。 这些对象是解释内存随时间增长的主要候选对象。

        走调用栈捕捉回溯通常需要大量计算。 因此,对于长寿命、高负载的应用程序来说,使用精确的泄漏检查是不可行的。 通过对分配进行统计采样,可以降低计算开销,同时大致了解应用程序是如何利用内存的。 有关控制采样间隔的信息,请参阅lg_prof_sample MALLOC_CONF  选项。

例子:

环境变量导入

环境变量导入,生成heap:

Background · jemalloc/jemalloc Wiki (github.com)

export MALLOC_CONF=prof_leak:true,lg_prof_sample:19,prof:true,prof_prefix:/mnt/nfs/profile/jeprof.out,prof_final:true,lg_prof_interval:30
export LD_PRELOAD=/mnt/nfs/libjemalloc.so.2

prof:true:启用配置文件。
prof_active:true:启用性能分析。
lg_prof_sample:0:设置采样率为最高。
tcache:false:禁用线程缓存,可能影响性能,但在进行性能分析时,可以提供更准确的内存分配数据。
prof_prefix:jeprof.out:指定性能分析输出文件前缀。

程序运行后查看是否挂载了jemalloc

lsof -p pid | grep je

纯代码生成

纯代码生成heap文件:

#include "jemalloc.h"
const char* malloc_conf = "prof:true,prof_active:false,prof_prefix:/tmp/jemallocdump";
//开启内存检测
static void startCheckHeapProfile()
{
    bool profSupport = false;
    size_t size = sizeof(bool);
    mallctl("config.prof", &profSupport, &size, NULL, 0);//编译是否支持
    bool profEnable = false;
    auto ret = mallctl("opt.prof", &profEnable, &size, NULL,0);//是否启用
    if(profSupport && profEnable)
    {
        bool profActive = false;
        ret = mallctl("prof.active", &profActive, &size, NULL, 0);
        if(profActive)
        {
            ret = mallctl("prof.dump", NULL, NULL, NULL, 0);
            if (ret != 0) {
                prinft("jemalloc failed to prof.dum");
            }
            return;
        }
        profActive = true;
        ret = mallctl("prof.active", NULL, 0, &profActive, sizeof(profActive));
        if (ret != 0) {
            prinft("jemalloc failed to enable prof.active.");
        }
        const char* path = "/tmp/profile";
        size = sizeof(path);
        ret = mallctl("prof.prefix", nullptr, nullptr, &path, size);
        if (ret != 0) {
             prinft( "jemalloc failed to set prof.prefix (jemalloc 5.3+ required).");
        }

        ret = mallctl("prof.dump", NULL, NULL, NULL, 0);
        if (ret != 0) {
            prinft("jemalloc failed to prof.dum");
        }
    }
}

//关闭内存检测
static void stopCheckHeapProfile()
{
    bool profActive = false;
    size_t size = sizeof(bool);
    auto ret = mallctl("prof.active", &profActive, &size, NULL, 0);
    if(profActive)
    {
        profActive = false;
        ret = mallctl("prof.active", NULL, 0, &profActive, sizeof(profActive));
        if (ret != 0) {
            prinft("jemalloc failed to enable prof.active.");
        }
        prinft("jemalloc stop heap profile");
    }
}

解析heap 

ubuntu先安装jemalloc:源码编译的jeprof无法运行

apt install libjemalloc-dev

 可能还需要安装

apt-get install gv
apt-get install dot

生成pdf:
注意:解析的total是从程序启动未释放的内存,而不是第一个dump到最后一个dump之间创建的内存。

# 单位为KB
    jeprof --lib_prefix=<lib_dir库路径> --show_bytes --pdf ./your_program  --base=first.heap end.heap >report.pdf
# 单位为M
    jeprof --debug --lib_prefix=<lib_dir库路径> --pdf ./your_program  --base=first.heap end.heap >report.pdf

生成text

jeprof --lib_prefix=<lib_dir库路径> --text ./your_program   --base=./core/prefix.10183.0.m0.heap ./core/prefix.10183.8372.m8372.heap >xxx.txt