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);
posted @ 2025-08-05 11:28  LdreamerD  阅读(79)  评论(0)    收藏  举报