jemalloc 内存分配器完全指南 / 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_prefix | profile 文件路径前缀 | "" |
prof_leak | 退出时输出泄漏报告 | false |
prof_final | 退出时输出最终 profile | true |
lg_prof_sample | 采样间隔的 log2(字节) | 19(即 512KB) |
prof_gdump | 每次新 extent 创建时 dump | false |
prof_accum | 记录累积分配(含已释放) | false |
prof_thread_active_init | 新线程是否默认激活 profiling | true |
5.3 采样机制
工作原理
jemalloc 使用 随机采样 而非全量记录来降低 profiling 的性能开销:
每次 malloc(size):
1. 生成随机数
2. 如果 random % sample_interval == 0:
- 记录调用栈 (backtrace)
- 记录分配大小
- 记录到 profile 数据结构中
3. 正常执行分配
每次 free(ptr):
1. 如果 ptr 被采样记录:
- 标记为已释放
- 更新 profile 数据
2. 正常执行释放
采样间隔选择
lg_prof_sample | 采样间隔 | 开销 | 适用场景 |
|---|---|---|---|
| 16 | 64 KB | 高 | 精细分析,短时间运行 |
| 19 | 512 KB | 低 | 默认值,适合大多数场景 |
| 22 | 4 MB | 极低 | 长时间运行的生产服务 |
| 24 | 16 MB | 几乎无 | 仅关注大对象泄漏 |
经验法则:采样间隔越小,数据越精确,但 CPU 和内存开销越大。生产环境建议
lg_prof_sample:19或lg_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 是线程安全的 |
| 不兼容 ASan | AddressSanitizer 会替换 malloc |
5.10 本章小结
| 工具/技术 | 用途 |
|---|---|
--enable-prof | 编译时启用 profiling |
lg_prof_sample | 控制采样精度 |
jeprof | 分析 heap profile 文件 |
prof_leak | 内存泄漏检测 |
kill -USR1 | 运行时触发 dump |
stats_print | 退出时打印统计 |
扩展阅读
上一章:第 4 章:配置详解 下一章:第 6 章:性能调优