jemalloc 内存分配器完全指南 / 03 - 架构与原理
第 3 章:架构与原理
3.1 整体架构概览
jemalloc 采用 分层、分区 的内存管理架构,核心目标是减少锁竞争和内存碎片。
┌─────────────────────────────────────────────────────────────┐
│ 应用层 (malloc / free) │
├─────────────────────────────────────────────────────────────┤
│ Thread Cache (TC) [无锁] │
│ ┌────────┬────────┬────────┐ │
│ │ Small │ Small │ Small │ ... │
│ │ List 0 │ List 1 │ List 2 │ │
│ └────────┴────────┴────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Arena 层 [带锁] │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Bin 0 (16B) │ Bin 1 (32B) │ Bin 2 (48B) │ ... │ │
│ │ ┌─────┐ │ ┌─────┐ │ ┌─────┐ │ │ │
│ │ │ Run │ │ │ Run │ │ │ Run │ │ │ │
│ │ └─────┘ │ └─────┘ │ └─────┘ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Large Object Allocation (直接从 extent 分配) │ │
│ └──────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Extent / Page 层 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Extent A (4 pages) │ Extent B (8 pages) │ ... │ │
│ └──────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 操作系统层 (mmap / sbrk) │
└─────────────────────────────────────────────────────────────┘
3.2 Arena
概念
Arena 是 jemalloc 中最核心的管理单元,每个 Arena 独立管理一块内存区域。其设计目的是让不同线程尽量使用不同的 Arena,从而减少锁竞争。
线程 0 ──→ Arena 0
线程 1 ──→ Arena 1
线程 2 ──→ Arena 2
线程 3 ──→ Arena 0 (轮询分配)
线程 4 ──→ Arena 1
...
Arena 数量
- 默认公式:
narenas = 4 * CPU 核心数 - 4 核 CPU → 16 个 Arena
- 64 核 CPU → 256 个 Arena
# 查看当前 Arena 数量
MALLOC_CONF="stats_print:true" ./my_program 2>&1 | grep "narenas"
# 或使用 jemalloc API
#include <jemalloc/jemalloc.h>
unsigned narenas;
size_t len = sizeof(narenas);
je_mallctl("arenas.narenas", &narenas, &len, NULL, 0);
printf("narenas = %u\n", narenas);
Arena 选择策略
- 首次分配:线程绑定到轮询选择的 Arena
- 后续分配:优先使用已绑定的 Arena
- 绑定改变:当线程 Arena 负载较重时,可能切换
3.3 线程缓存 (Thread Cache / TC)
原理
Thread Cache 是每个线程独享的内存缓存层,无需加锁即可完成分配和释放。
malloc(64B)
→ 检查 Thread Cache 是否有 64B 空闲块
→ 有:直接返回(无锁,极快)
→ 没有:从 Arena 的 Bin 中批量获取一批(加锁)
TC 结构
每个 TC 为每个大小类维护一个 freelist:
Thread Cache
├── Bin 0 (16B): [chunk] → [chunk] → [chunk] → NULL
├── Bin 1 (32B): [chunk] → [chunk] → NULL
├── Bin 2 (48B): [chunk] → NULL
├── ...
└── Bin N (large): 不经过 TC,直接走 Arena
TC 大小限制
| 参数 | 默认值 | 说明 |
|---|---|---|
tcache_max | 32KB (64位) | 超过此大小的对象不使用 TC |
tcache_gc_incr_bytes | 4096 | GC 触发增量 |
tcache_gc_delay_bytes | 65536 | GC 延迟字节数 |
# 查看 TC 统计
MALLOC_CONF="stats_print:true,tcache_max:65536" ./my_program 2>&1 | grep -A 20 "tcache"
3.4 大小类 (Size Class)
设计理念
jemalloc 将内存分配请求按大小分为固定类别,每个类别称为一个 大小类。这样可以:
- 减少内部碎片
- 提高 Slab 分配效率
- 简化空闲块管理
大小类表(64 位系统)
jemalloc 使用三组递增的大小类:
| 范围 | 间隔 | 说明 |
|---|---|---|
| 0 - 128 B | 16 B | Tiny class(极小对象) |
| 128 B - 4 KB | 倍增 | Small class(小对象) |
| 4 KB - 32 KB | 4 KB | Small class(小对象) |
| 32 KB+ | 按页对齐 | Large class(大对象) |
完整大小类(部分):
Index Size Index Size Index Size
----- ---- ----- ---- ----- ----
0 16 B 10 160 B 20 2.25 KB
1 32 B 11 192 B 21 2.75 KB
2 48 B 12 224 B 22 3.25 KB
3 64 B 13 256 B 23 3.75 KB
4 80 B 14 320 B 24 4.25 KB
5 96 B 15 384 B 25 5.25 KB
6 112 B 16 448 B 26 6.25 KB
7 128 B 17 512 B 27 7.25 KB
8 160 B 18 768 B 28 8.25 KB
9 128 B 19 1024 B 29 10.25 KB
查询大小类
#include <jemalloc/jemalloc.h>
#include <stdio.h>
int main() {
// 查询某个大小属于哪个 size class
for (size_t sz = 1; sz <= 256; sz++) {
size_t actual = je_s2u(sz); // slab 下界
printf("requested: %4zu -> actual: %4zu\n", sz, actual);
}
// 查询指定 index 对应的大小
unsigned nclasses;
size_t len = sizeof(nclasses);
je_mallctl("arenas.nbins", &nclasses, &len, NULL, 0);
printf("Number of bin size classes: %u\n", nclasses);
for (unsigned i = 0; i < nclasses; i++) {
size_t sz;
char cmd[64];
snprintf(cmd, sizeof(cmd), "arenas.bin.%u.size", i);
len = sizeof(sz);
je_mallctl(cmd, &sz, &len, NULL, 0);
printf(" bin %2u: %6zu bytes\n", i, sz);
}
return 0;
}
gcc -o size_class size_class.c -ljemalloc
./size_class
3.5 Slab 分配
概念
Slab(也称为 Run)是一段连续的内存页,被切分为等大小的块 (chunk)。每个 Slab 属于一个大小类 (Bin)。
一个 4 页 (16KB) 的 Slab,属于 64B 大小类:
┌────────────────────────────────────────────────────────────────────┐
│ Bitmap: [1][1][0][0][1][1][0][0][0][1][1][0]... │
├────────┬────────┬────────┬────────┬────────┬────────┬────────┬────┤
│ 64B │ 64B │ 64B │ 64B │ 64B │ 64B │ 64B │...│
│ (已用) │ (已用) │ (空闲) │ (空闲) │ (已用) │ (已用) │ (空闲) │...│
└────────┴────────┴────────┴────────┴────────┴────────┴────────┴────┘
Bitmap 管理
jemalloc 使用 bitmap 跟踪 Slab 中每个块的使用状态:
1= 已分配0= 空闲
分配时扫描 bitmap 找到第一个 0 位,释放时将对应位清零。
3.6 页 (Page) 与 Extent
页 (Page)
- 操作系统的内存管理以 页 (Page) 为单位,通常为 4KB
- jemalloc 也以页为基本管理单位
Extent
Extent 是 jemalloc 管理连续内存区域的基本单元,大小为页的整数倍。
| 层级 | 大小 | 说明 |
|---|---|---|
| Page | 4 KB | 基本管理单位 |
| Slab (Run) | 4-32 KB | 小对象的容器 |
| Extent | 64 KB - 数 MB | 从 OS 获取的大块内存 |
| Chunk (旧称) | 4 MB (默认) | jemalloc 4.x 的术语 |
Extent (256 KB = 64 pages)
├── Slab A (16 KB, 64B class, 256 blocks)
├── Slab B (16 KB, 128B class, 128 blocks)
├── [unused pages]
└── Slab C (32 KB, 256B class, 128 blocks)
Extent 分配方式
| 方式 | 说明 |
|---|---|
mmap | 从操作系统获取新的虚拟内存 |
madvise(MADV_DONTNEED) | 告诉 OS 可回收这些页(脏页回收) |
| 拆分 (Split) | 将一个大 extent 拆分为多个小 extent |
| 合并 (Merge) | 将相邻的空闲 extent 合并为大 extent |
3.7 对象分类与分配路径
分类标准
┌─────────────────────────────┐
│ malloc(size) 请求 │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ size <= tcache_max? │
└──┬──────────────────────┬───┘
│ YES │ NO
┌────────▼────────┐ ┌───────▼────────┐
│ 通过 Thread │ │ 直接走 Arena │
│ Cache 分配 │ │ 分配 │
└────────┬────────┘ └───────┬────────┘
│ │
┌─────────▼─────────┐ ┌────────▼────────┐
│ size <= 14KB? │ │ Large Object │
└──┬────────────┬───┘ │ 直接分配 Extent│
│ YES │ NO └─────────────────┘
┌────────▼──────┐ ┌─▼──────────┐
│ Small Object │ │ Large │
│ 从 Slab 分配 │ │ 从 Extent │
└───────────────┘ │ 直接分配 │
└────────────┘
三种分配路径
| 类型 | 大小范围 | 分配来源 | 加锁 |
|---|---|---|---|
| Tiny | ≤ 128 B | Thread Cache → Arena Slab | TC 无锁,Arena 有锁 |
| Small | 128 B - 14 KB | Thread Cache → Arena Slab | TC 无锁,Arena 有锁 |
| Large | > 14 KB | Arena → Extent | 有锁 |
注意:大小范围是近似值,具体取决于页大小和
tcache_max配置。
3.8 大对象 (Large Object) 管理
分配流程
malloc(1MB)
→ 进入 Arena 的 large 分配路径
→ 查找 cached extent(之前释放但未归还 OS 的 extent)
→ 有合适的:直接使用
→ 没有:通过 mmap 向 OS 申请
→ 返回 extent 的首地址
大对象的特殊性
- 不经过 Thread Cache:大对象占用空间大,缓存会浪费内存
- 直接映射到页:大对象通常按页对齐
- 使用独立的 LRU 管理:方便按需归还给操作系统
- 碎片问题更突出:大对象释放后的空洞难以被其他大小利用
3.9 命名规则术语表
| 术语 | 说明 |
|---|---|
| Arena | 独立的内存管理区域,包含多个 Bin 和 Extent |
| Bin | 管理同一大小类的容器,包含多个 Slab |
| Slab / Run | 切分为等大小块的连续页 |
| Extent | 从 OS 获取的连续内存区域 |
| TC (Thread Cache) | 线程本地缓存,无锁访问 |
| Size Class | 预定义的分配大小类别 |
| Dirty Page | 已释放但未归还 OS 的页 |
| Clean Page | 已归还 OS(通过 madvise)的页 |
| Bitmap | 跟踪 Slab 中块使用状态的位图 |
| Flush | 将 TC 中的空闲块归还 Arena |
3.10 分配与释放的完整流程
malloc 流程
void *malloc(size_t size) {
// 1. 将 size 向上取整到最近的 size class
size_t sz = sz_size2index(size);
// 2. 如果 sz <= tcache_max,走 Thread Cache
if (sz <= tcache_max) {
void *ptr = tcache_alloc(sz);
if (ptr) return ptr;
// TC 空,需要从 Arena 填充
return arena_tcache_fill_small(sz);
}
// 3. 如果 size < 大对象阈值,走 Arena small 分配
if (sz < large_threshold) {
return arena_malloc_small(sz);
}
// 4. 大对象,直接分配 Extent
return arena_malloc_large(sz);
}
free 流程
void free(void *ptr) {
// 1. 查找 ptr 所属的 extent
extent_t *extent = iealloc(ptr);
// 2. 如果是小对象且 TC 未满,放入 TC
if (extent->sz_size <= tcache_max) {
if (tcache_dalloc(extent->sz_size, ptr)) return;
// TC 已满,需要 flush
tcache_flush(extent->sz_size);
}
// 3. 小对象放回 Arena Slab
if (extent->sz_size < large_threshold) {
arena_dalloc_small(extent, ptr);
return;
}
// 4. 大对象归还 Extent
arena_dalloc_large(extent);
}
3.11 验证架构理解
// arch_demo.c - 观察 jemalloc 的分层统计
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef USE_JEMALLOC
#include <jemalloc/jemalloc.h>
#endif
#define N 100000
int main() {
void *ptrs[N];
// 分配不同大小的对象
for (int i = 0; i < N; i++) {
size_t sz = (i % 3 == 0) ? 32 : (i % 3 == 1) ? 256 : 8192;
ptrs[i] = malloc(sz);
if (ptrs[i]) memset(ptrs[i], 0xAB, sz);
}
// 释放一半
for (int i = 0; i < N; i += 2) {
free(ptrs[i]);
}
#ifdef USE_JEMALLOC
printf("=== jemalloc Statistics ===\n");
je_malloc_stats_print(NULL, NULL, NULL);
#endif
// 清理
for (int i = 1; i < N; i += 2) {
free(ptrs[i]);
}
return 0;
}
gcc -O2 -DUSE_JEMALLOC -o arch_demo arch_demo.c -ljemalloc
./arch_demo 2>&1 | head -80
3.12 本章小结
| 组件 | 职责 | 关键特点 |
|---|---|---|
| Arena | 管理内存区域 | 多实例,减少锁竞争 |
| Thread Cache | 线程本地缓存 | 无锁,快速分配小对象 |
| Bin | 管理同一大小类 | 包含多个 Slab |
| Slab (Run) | 切分为等大小块 | 使用 Bitmap 跟踪状态 |
| Extent | 管理连续内存页 | 支持拆分/合并 |
| Size Class | 预定义大小类别 | 减少碎片 |
扩展阅读
上一章:第 2 章:安装与编译 下一章:第 4 章:配置详解