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

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 选择策略

  1. 首次分配:线程绑定到轮询选择的 Arena
  2. 后续分配:优先使用已绑定的 Arena
  3. 绑定改变:当线程 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_max32KB (64位)超过此大小的对象不使用 TC
tcache_gc_incr_bytes4096GC 触发增量
tcache_gc_delay_bytes65536GC 延迟字节数
# 查看 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 B16 BTiny class(极小对象)
128 B - 4 KB倍增Small class(小对象)
4 KB - 32 KB4 KBSmall 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 管理连续内存区域的基本单元,大小为页的整数倍。

层级大小说明
Page4 KB基本管理单位
Slab (Run)4-32 KB小对象的容器
Extent64 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 BThread Cache → Arena SlabTC 无锁,Arena 有锁
Small128 B - 14 KBThread Cache → Arena SlabTC 无锁,Arena 有锁
Large> 14 KBArena → Extent有锁

注意:大小范围是近似值,具体取决于页大小和 tcache_max 配置。


3.8 大对象 (Large Object) 管理

分配流程

malloc(1MB)
  → 进入 Arena 的 large 分配路径
    → 查找 cached extent(之前释放但未归还 OS 的 extent)
      → 有合适的:直接使用
      → 没有:通过 mmap 向 OS 申请
    → 返回 extent 的首地址

大对象的特殊性

  1. 不经过 Thread Cache:大对象占用空间大,缓存会浪费内存
  2. 直接映射到页:大对象通常按页对齐
  3. 使用独立的 LRU 管理:方便按需归还给操作系统
  4. 碎片问题更突出:大对象释放后的空洞难以被其他大小利用

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预定义大小类别减少碎片

扩展阅读

  1. jemalloc 内部实现详解 (官方 Wiki)
  2. 内存分配器设计与实现
  3. Linux 内存管理 - mmap 与 brk

上一章第 2 章:安装与编译 下一章第 4 章:配置详解