jemalloc原理
┌──────────────────────────────────────┐
│ jemalloc │
└──────────────────────────────────────┘
│
┌─────────┴─────────┐
│ arena[N] │ ← 多线程隔离的分配区域,每个线程绑定一个 arena
└─────────┬─────────┘
│
┌───────────────────┴───────────────────┐
│ bin[K] │ ← 每种大小的对象由不同 bin 管理,每个bin[i]管理同一大小类别的对象
└───────────────────┬───────────────────┘
│
┌────────────┴────────────┐
│ slab │ ← 每 slab 是多个 block 的容器
└────────────┬────────────┘
│
┌───────────┴───────────┐
│ block │ ← 实际返回给用户使用的对象
└───────────────────────┘
1️⃣ arena:多线程分配隔离区域
每个线程在第一次分配时,jemalloc 会为它分配一个 arena;
arena 是 jemalloc 的最高层分配器,负责管理多个 bin;
arena 数量默认根据 CPU 核心数决定,可通过 narenas 配置调整。
好处:
减少线程之间的锁竞争;
避免 cache ping-pong。
2️⃣ bin:按大小分类的分配池
jemalloc 预定义了多个大小类别(size class),常见的如:
8, 16, 32, 64, 128, 256, 512, 1024, ..., 4MB
每个 bin 管理一种大小;
bin 下管理多个 slab,每个 slab 可分成若干固定大小 block。
默认情况下:
bin 对应大小
0 8 B
1 16 B
2 32 B
... ...
25 4096 B
jemalloc 的 jemalloc/include/jemalloc/jemalloc_macros.h.in 中定义了各个 bin 的默认大小。
3️⃣ slab:多个 block 的容器
每个 slab 是 1 个或多个 page(通常 4KB)的连续内存块;
每个 slab 只用于一个 bin;
slab 会通过 bitmap 管理 block 的分配状态(空闲 / 使用中);
当 slab 中所有 block 都是空闲的,就可以被回收(madvise() 释放回 OS)。
注意:slab 回收是 jemalloc 减少 RSS 的关键。
4️⃣ block:实际分配给你的内存
jemalloc 分配返回的就是某个 slab 上的一个 block;
malloc(128) → 找到适合的 bin → 在该 bin 的某个 slab 上找空闲 block → 返回。
📦 示例:你调用 new InternalEntry[64] 会发生什么?
假设 InternalEntry 是 64 字节,new InternalEntry[64] 需要 64 × 64 = 4096 字节。
jemalloc 分配流程如下:
调用 malloc(4096);
jemalloc 发现 4096 属于 小对象(≤ 4KB),进入 small bin;
找到最近的 size class,正好是 4096;
在当前线程的 arena 里找到对应的 bin;
该 bin 有一个或多个 slab,每个 slab 划分成 4096 字节的 block;
找 slab 中空闲 block,返回一个地址给你;
jemalloc 在 slab 内部 bitmap 中标记该 block 为已使用。
🗑 示例:你调用 delete[] entrys,会发生什么?
jemalloc 根据指针 entrys 的地址快速查出其所在的 slab;
标记对应 block 为“空闲”;
放回 bin 的 free list;
❗ slab 仍然无法释放,因为:
它可能还有其他 block 在用;
slab 是整体分配、整体管理的,不能部分释放;
如果 slab 上所有 block 都空了,jemalloc 才会调用 madvise() 将物理内存还给 OS;
否则该 slab 保留在 bin 内备用,造成 RSS 不下降但程序逻辑已释放 的假象。
🧼 jemalloc slab 回收条件(释放 RSS)
情况 slab 是否释放? RSS 下降?
block 释放,但 slab 还有其他 block 在用 ❌ ❌
slab 所有 block 都空闲 ✅ ✅(madvise)
调用 mallctl("arena.0.purge", ...) 如果 slab 满足释放条件,则释放 ✅
启用 tcache:false 且主动清理 更容易释放 slab ✅
🧠 关键例子:为何“析构老对象 + 重建新对象”能释放 slab?
假设你如下操作:
KeyMaps* km = new KeyMaps(); // Put 2700W keys
// Del 删除 1500W key
km->FreeDeletedMem(); // 试图释放多余空间
// 此时 delete[] entrys 多次,但 slab 无法释放
KeyMaps* new_km = new KeyMaps(*km); // 拷贝新对象,只复制有效数据
delete km; // 删除老对象,所有 slab block 都 free
发生了什么?
老对象中的 slab(如 slab0、slab1)被多个 KV[i] 的 InternalEntry[] 持有;
你调用 delete[] entrys,只是把 block 回收到 jemalloc,但 slab 还“部分使用”;
析构整个 KeyMaps 后,所有 KV[i] 被清空,delete[] 最终让 slab 上所有 block 空;
jemalloc 发现 slab 空了,就调用 madvise;
操作系统释放物理页,RSS 下降。
🔍 jemalloc 中的回收机制(和 tcache 的关系)
jemalloc 会优先使用线程本地的 tcache 缓存;
释放 block 时并不立即归还 bin,而是放进 tcache;
tcache 满了,jemalloc 才将 block 送回 slab;
所以:
小对象频繁 new/delete 可能 只是互相复用 tcache 中的 block;
slab 上的空 block 不回到 bin,不回到 slab bitmap;
slab 永远不会空 → RSS 不降。
解决方案:
设置:MALLOC_CONF="tcache:false",彻底关闭线程缓存;
或用 mallctl("tcache.flush", ...) 主动清理。
📊 jemalloc 大小类别(bin)参考表
| 分配请求 | 落在哪个 bin | slab block 大小 | slab 大小(通常) |
|---|---|---|---|
| 64B | bin 3 | 64B | 8KB / 128 block |
| 256B | bin 6 | 256B | 16KB / 64 block |
| 512B | bin 7 | 512B | 32KB / 64 block |
| 4096B | bin 9 | 4KB | 64KB / 16 block |
| > 8KB | large alloc | 直接 mmap | 自成 slab / chunk |
🔧 如何调试 / 控制 jemalloc slab
| 操作 | 方法 |
|---|---|
| 启用 profiling | MALLOC_CONF="prof:true" |
| 关闭 tcache | MALLOC_CONF="tcache:false" |
| 设置 decay 时间为 0(加速回收) | MALLOC_CONF="dirty_decay_ms:0,muzzy_decay_ms:0" |
| 强制 purge 某个 arena | mallctl("arena.0.purge", ...) |
| 获取当前使用的 arena | mallctl("thread.arena", ...) |
| 查看 heap 使用状态 | jeprof, heaptrack, valgrind massif |
✅ 总结
| 组件 | 含义 |
|---|---|
| arena | 分配线程隔离,独立内存池 |
| bin | 不同大小的对象分类管理 |
| slab | 固定大小 block 的内存页组 |
| block | 实际分配给用户的最小单位 |
| tcache | 每线程缓存层,避免频繁锁竞争 |
| madvise(MADV_DONTNEED) | slab 变空后回收 RSS 的关键机制 |
如你想深入代码,可以重点看:
src/arena.c:arena/slab 管理
src/extent.c:mmap, madvise 调用
src/tcache.c:线程缓存控制
src/prof.c:profile dump 与 jeprof 关系
二、为什么要有 slab,而不是直接 bin→block?
这是你的关键问题。
❓如果 bin 直接管理所有 block 会怎样?
那它需要管理一个 超级大数组,包括:
所有 malloc(64B) 的 block;
所有归还的空闲块;
管理代价大,内存不局部,性能低。
✅ 引入 slab 的好处:
| slab 作为中间层的意义 | 解释 |
|---|---|
| 分段管理 block | 每个 slab 只管理几十或几百个 block,bin 不需要维护超大数组 |
| 局部性优化 | block 聚集在 slab 中,cache 友好,TLB 命中率高 |
| 易于 slab 回收 | 如果 slab 的所有 block 空闲 → 可以整个 slab madvise 回 OS |
| 多 slab 并发管理 | 多线程 arena 可以同时操作不同 slab,提高并发性 |
📐 三、举个例子说明:
void* p = malloc(128);
假设 jemalloc 预设 128B 落在 bin[6]:
bin[6] 会有若干个 slab;
每个 slab 是 16KB 大小,被划成 128B 的 block:
一个 slab = 16KB / 128B = 128 个 block;
slab 内有一个 bitmap:
0 1 1 0 1 0 ... // 1 表示已分配,0 表示空闲
jemalloc 在 bitmap 中查找空闲位 → 返回一个 block;
用户获取指针 p,其实就是指向 slab 的一个 128B 块。
📊 四、与 Linux 伙伴算法的对比
特点 jemalloc Linux 伙伴系统
管理单位 bin / slab / block 伙伴页框(2^N page)
分配粒度 精准固定大小 block(8B ~ 4MB) 页级别(4KB、8KB、...)
回收机制 slab 全空时才回收给 OS(madvise) 伙伴页对齐且空闲才合并
局部性优化 slab 结构强制对象集中分配 没有 slab 层次聚合
使用 bitmap bin 管理 slab 的 block zone 管理 page frame 使用 bitmap
结论:
✅ jemalloc 是用户态堆内存分配器,它 不关心 page 分配,而是从 Linux 请求大块(chunk/extent),在用户态模拟出类似 slab/bitmap 的系统,并做得更精细化、性能更好。
🧠 五、为什么 narenas = 257,你只有一个线程?
实测中通过mallctl("arenas.narenas", &narenas, &sz, nullptr, 0)
结果是:narenas = 257
✅ 解释如下:
jemalloc 默认创建 固定数量的 arena,以提升并发性能;
默认值是:narenas = 2 * number_of_cpu_cores,并提前分配出来;
例如:你的机器是 128 核 → 2 × 128 + 1(arena[0]) = 257;
每个线程使用 mallctl("thread.arena", ...) 绑定一个 arena;
你虽然只用了一个线程,jemalloc 仍然初始化了所有 arena;
实际使用的 arena 数量远远少于 257,大多数是空的。
🔧 如何查看当前线程用的是哪个 arena?
unsigned arena_index;
size_t sz = sizeof(arena_index);
mallctl("thread.arena", &arena_index, &sz, nullptr, 0);
printf("Current thread arena: %u\n", arena_index);

浙公网安备 33010602011771号