强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

05 - 内存分析与 Profiling

第 5 章:内存分析与 Profiling

5.1 为什么需要内存分析

问题表现后果
内存泄漏RSS 持续增长OOM Kill,服务不可用
内存碎片RSS 远大于实际使用量资源浪费,容量规划失准
热点分配某些路径频繁分配/释放CPU 浪费在内存管理上
大对象泄漏某些对象长期不释放可用内存持续减少

jemalloc 内置的 profiling 能力可以精确定位这些问题。


5.2 启用 Profiling

编译时启用

# 必须在编译时开启 --enable-prof
./configure --prefix=/usr/local --enable-prof
make -j$(nproc) && sudo make install && sudo ldconfig

运行时启用

# 基本启用
export MALLOC_CONF="prof:true,prof_active:true,prof_prefix:/tmp/jeprof"

# 完整配置
export MALLOC_CONF="\
prof:true,\
prof_active:true,\
prof_prefix:/tmp/jeprof,\
prof_leak:true,\
prof_final:true,\
lg_prof_sample:19"
参数说明默认值
prof启用 profiling 功能false
prof_active激活采样(可运行时切换)true
prof_prefixprofile 文件路径前缀""
prof_leak退出时输出泄漏报告false
prof_final退出时输出最终 profiletrue
lg_prof_sample采样间隔的 log2(字节)19(即 512KB)
prof_gdump每次新 extent 创建时 dumpfalse
prof_accum记录累积分配(含已释放)false
prof_thread_active_init新线程是否默认激活 profilingtrue

5.3 采样机制

工作原理

jemalloc 使用 随机采样 而非全量记录来降低 profiling 的性能开销:

每次 malloc(size):
  1. 生成随机数
  2. 如果 random % sample_interval == 0:
     - 记录调用栈 (backtrace)
     - 记录分配大小
     - 记录到 profile 数据结构中
  3. 正常执行分配

每次 free(ptr):
  1. 如果 ptr 被采样记录:
     - 标记为已释放
     - 更新 profile 数据
  2. 正常执行释放

采样间隔选择

lg_prof_sample采样间隔开销适用场景
1664 KB精细分析,短时间运行
19512 KB默认值,适合大多数场景
224 MB极低长时间运行的生产服务
2416 MB几乎无仅关注大对象泄漏

经验法则:采样间隔越小,数据越精确,但 CPU 和内存开销越大。生产环境建议 lg_prof_sample:19lg_prof_sample:22


5.4 使用 jeprof 分析 Profile

5.4.1 jeprof 工具简介

jemalloc 自带 jeprof 工具(通常安装在 bin/jeprof),用于分析 profile 输出文件。

# 确认 jeprof 位置
which jeprof
# /usr/local/bin/jeprof

# 或从源码目录
ls jemalloc/bin/jeprof

注意jeprof 依赖 Perl 和 dot(Graphviz),需要安装:

sudo apt install perl graphviz  # Debian/Ubuntu
sudo dnf install perl graphviz  # CentOS/Fedora

5.4.2 生成 Profile 文件

# 运行带 profiling 的程序
MALLOC_CONF="prof:true,prof_active:true,prof_prefix:/tmp/jeprof" ./my_program

# 查看生成的 profile 文件
ls -la /tmp/jeprof.*
# /tmp/jeprof.0.0.0.0.17.abc123.heap   <- 堆 profile

5.4.3 常用 jeprof 命令

# 1. 文本报告:按分配字节数排序的调用栈
jeprof --text /usr/local/bin/my_program /tmp/jeprof.*.heap

# 2. 生成调用图(PDF)
jeprof --pdf /usr/local/bin/my_program /tmp/jeprof.*.heap > heap.pdf

# 3. 生成 SVG
jeprof --svg /usr/local/bin/my_program /tmp/jeprof.*.heap > heap.svg

# 4. 生成 PostScript
jeprof --ps /usr/local/bin/my_program /tmp/jeprof.*.heap > heap.ps

# 5. Web 交互界面(推荐)
jeprof --web /usr/local/bin/my_program /tmp/jeprof.*.heap

# 6. 比较两个时间点的 profile(检测泄漏)
jeprof --base=/tmp/jeprof.0.0.0.0.10.aaa.heap \
       --text /usr/local/bin/my_program \
       /tmp/jeprof.0.0.0.0.20.bbb.heap

5.4.4 jeprof 输出解读

jeprof --text ./server /tmp/jeprof.*.heap

输出示例:

Total: 512.0 MB
300.0  58.6%  58.6%   300.0  58.6% ::std::vector::resize
100.0  19.5%  78.1%   100.0  19.5% ::createBuffer
 50.0   9.8%  87.9%    50.0   9.8% ::loadConfig
 30.0   5.9%  93.8%    30.0   5.9% ::handleRequest
 20.0   3.9%  97.7%   400.0  78.1% ::main
 12.0   2.3% 100.0%    12.0   2.3% ::initLogger
含义
第 1 列该函数自身分配的字节数(MB)
第 2 列占总分配的百分比
第 3 列累积百分比
第 4 列该函数及其调用链分配的总字节数
第 5 列累积百分比
第 6 列函数名

5.5 内存泄漏检测

5.5.1 启用泄漏报告

export MALLOC_CONF="prof:true,prof_active:true,prof_leak:true,prof_prefix:/tmp/jeprof"

程序正常退出时,jemalloc 会输出:

<jemalloc>: Leak of 1048576 bytes, detected at:
    @ 0x401234 allocate_buffer
    @ 0x401300 process_request
    @ 0x401400 main

5.5.2 两时间点对比检测泄漏

更精确的泄漏检测方法是在两个时间点分别 dump,然后对比:

# 在程序启动后的一段时间点
kill -SIGUSR1 <pid>  # 触发 dump(需要 prof_gdump:true)

# 或通过 API 触发
// 泄漏检测辅助函数
#include <jemalloc/jemalloc.h>
#include <signal.h>
#include <stdio.h>

static void dump_profile(int sig) {
    static int count = 0;
    char filename[256];
    snprintf(filename, sizeof(filename), "/tmp/jeprof.%d.heap", count++);
    je_mallctl("prof.dump", NULL, NULL, &filename, sizeof(filename));
    fprintf(stderr, "Profile dumped to %s\n", filename);
}

int main() {
    // 注册信号处理
    signal(SIGUSR1, dump_profile);
    signal(SIGUSR2, dump_profile);

    // ... 正常业务逻辑 ...

    return 0;
}
# 运行程序
MALLOC_CONF="prof:true,prof_active:true,prof_prefix:/tmp/jeprof" ./my_server &

# 等待一段时间后触发 dump
kill -USR1 $!
sleep 60
kill -USR1 $!

# 对比两个 dump
jeprof --base=/tmp/jeprof.0.heap --text ./my_server /tmp/jeprof.1.heap

5.5.3 Valgrind 与 jemalloc 配合

注意:Valgrind 的 memcheck 与 jemalloc 不兼容(两者都替换 malloc)。使用 jemalloc 的内置 leak check 或切换到系统分配器 + Valgrind。

# 方法 1:使用 jemalloc 内置 leak check
MALLOC_CONF="prof:true,prof_leak:true" ./my_program

# 方法 2:切换到系统分配器用 Valgrind
valgrind --leak-check=full ./my_program

5.6 运行时统计

5.6.1 mallinfo 接口

#include <malloc.h>
#include <stdio.h>

int main() {
    struct mallinfo mi = mallinfo();
    printf("arena:     %d bytes (total from mmap/sbrk)\n", mi.arena);
    printf("ordblks:   %d (free chunks)\n", mi.ordblks);
    printf("hblkhd:    %d bytes (mmap allocated)\n", mi.hblkhd);
    printf("uordblks:  %d bytes (in-use)\n", mi.uordblks);
    printf("fordblks:  %d bytes (free)\n", mi.fordblks);
    return 0;
}

5.6.2 mallctl 统计接口

#include <jemalloc/jemalloc.h>
#include <stdio.h>

void print_jemalloc_stats() {
    // 触发 epoch 更新统计
    uint64_t epoch = 1;
    size_t sz = sizeof(epoch);
    je_mallctl("epoch", &epoch, &sz, &epoch, sz);

    // 基本统计
    size_t allocated, active, metadata, resident, mapped;
    sz = sizeof(size_t);

    je_mallctl("stats.allocated", &allocated, &sz, NULL, 0);
    je_mallctl("stats.active",    &active,    &sz, NULL, 0);
    je_mallctl("stats.metadata",  &metadata,  &sz, NULL, 0);
    je_mallctl("stats.resident",  &resident,  &sz, NULL, 0);
    je_mallctl("stats.mapped",    &mapped,    &sz, NULL, 0);

    printf("=== jemalloc Stats ===\n");
    printf("allocated: %12zu bytes (%6.1f MB)\n", allocated, allocated / 1048576.0);
    printf("active:    %12zu bytes (%6.1f MB)\n", active,    active / 1048576.0);
    printf("metadata:  %12zu bytes (%6.1f MB)\n", metadata,  metadata / 1048576.0);
    printf("resident:  %12zu bytes (%6.1f MB)\n", resident,  resident / 1048576.0);
    printf("mapped:    %12zu bytes (%6.1f MB)\n", mapped,    mapped / 1048576.0);
}

5.6.3 退出时自动打印

# 最简方式
MALLOC_CONF="stats_print:true" ./my_program

# 详细输出
MALLOC_CONF="stats_print:true,stats_print_opts:mdalx" ./my_program
选项字母含义
m打印 mallctl 可查询的选项
d打印每个 Arena 的统计
a打印 Arena 概要
l打印大对象统计
x打印 extent 详细信息
b打印 Bin 统计
g打印配置信息

5.7 完整 profiling 工作流

步骤 1:编译带 profiling 的程序

gcc -O2 -g -o server server.c -ljemalloc

步骤 2:运行并采集 profile

MALLOC_CONF="prof:true,prof_active:true,lg_prof_sample:19,prof_prefix:/tmp/server_prof" \
  ./server &

SERVER_PID=$!

# 运行一段时间后,触发手动 dump
kill -USR1 $SERVER_PID

# 继续运行...
sleep 60

# 再次 dump(用于对比分析)
kill -USR1 $SERVER_PID

步骤 3:分析 profile

# 文本概览
jeprof --text ./server /tmp/server_prof.*.heap

# 可视化调用图
jeprof --svg ./server /tmp/server_prof.*.heap > profile.svg

# 对比分析(找泄漏)
jeprof --base=/tmp/server_prof.0.*.heap \
       --text ./server \
       /tmp/server_prof.1.*.heap

步骤 4:清理

kill $SERVER_PID
rm /tmp/server_prof.*.heap

5.8 业务场景示例

场景:Redis 内存分析

# 编译带 profiling 的 Redis(需要自行编译,而非包管理器版本)
cd redis-7.0
make MALLOC=jemalloc CFLAGS="-g -O2"

# 启动 Redis 并开启 profiling
MALLOC_CONF="prof:true,prof_active:true,lg_prof_sample:20,prof_prefix:/tmp/redis_prof" \
  ./src/redis-server &

# 运行 benchmark
./src/redis-benchmark -c 50 -n 1000000 -d 256

# dump profile
kill -USR1 $(pgrep redis-server)

# 分析
jeprof --text ./src/redis-server /tmp/redis_prof.*.heap
jeprof --svg ./src/redis-server /tmp/redis_prof.*.heap > redis_heap.svg

5.9 Profiling 注意事项

要点说明
必须编译时启用--enable-prof 无法运行时开启
需要调试符号编译时加 -g,否则看不到函数名
性能开销采样模式下约 2-5%,可接受
磁盘空间heap 文件可能很大,注意清理
信号触发SIGUSR1 触发 dump 时会暂停程序
多线程安全jemalloc 的 profiling 是线程安全的
不兼容 ASanAddressSanitizer 会替换 malloc

5.10 本章小结

工具/技术用途
--enable-prof编译时启用 profiling
lg_prof_sample控制采样精度
jeprof分析 heap profile 文件
prof_leak内存泄漏检测
kill -USR1运行时触发 dump
stats_print退出时打印统计

扩展阅读

  1. jemalloc Heap Profiling (官方文档)
  2. jeprof 使用详解
  3. gperftools heap profiling 对比

上一章第 4 章:配置详解 下一章第 6 章:性能调优