内存分配器深度解析
ptmalloc  ·  tcmalloc  ·  jemalloc

架构原理 · 核心工作流程 · 锁模型 · 性能对比 · 适用场景

🔶 ptmalloc — glibc 默认 🟣 tcmalloc — Google / Go 🔵 jemalloc — Firefox / Redis / FreeBSD
📖 三大分配器概述
ptmalloc2
glibc 内置 1996 → 至今

Linux 系统默认内存分配器,是 dlmalloc 的多线程演进版本。以 Arena(竞技场) 为核心隔离单元,每个线程优先绑定到一个 Arena,通过 mutex 保护 Arena 内的所有操作。

  • 作者:Wolfram Gloger 改进自 Doug Lea
  • 核心结构:Arena → Heap → Chunk
  • 最大问题:Arena 数固定,锁竞争激烈
  • 内存不跨 Arena 归还,碎片化严重
tcmalloc
Google 出品 Go runtime 使用

Thread-Caching Malloc,Google 为解决高并发场景下的锁争用问题而设计。核心思路:每个线程拥有私有缓存,绝大多数分配/释放无需加锁,仅在缓存补充/归还时访问中央堆。

  • 作者:Sanjay Ghemawat(Google)
  • 核心结构:ThreadCache → CentralCache → PageHeap
  • 小对象(≤256KB)完全无锁操作
  • Go 语言运行时基于其思想深度定制
jemalloc
Jason Evans Redis / Firefox

Jason Evans 为 FreeBSD 设计,后被 Facebook/Meta 大规模优化。以 多 Arena + 精细 Size Class + 线程缓存 三层结构为特征,在降低碎片率与高并发间取得最佳平衡。

  • 作者:Jason Evans(2006,FreeBSD)
  • 核心结构:tcache → Arena → Chunk → Run
  • Size Class 极细,碎片率业界最低
  • 内置详尽的统计与监控接口
🏗️ 内存架构图
ptmalloc2 — Arena 架构
ptmalloc2 内存布局
🏟️ Main Arena(主竞技场)— 唯一可直接扩展 brk/mmap
Thread Arena 1
mutex 独立
Thread Arena 2
mutex 独立

上限 8×CPU核
▼ 每个 Arena 内部
📦 Heap 1
连续内存块
📦 Heap 2
mmap 映射
📦 Heap n
...
▼ Chunk 分类管理
fast bins
16~80B
单链表 LIFO
small bins
≤504B
双向循环链表
large bins
≥512B
按大小有序链表
unsorted bin
free 时临时
待分类
top chunk
堆顶剩余
最后使用
⚠️ 关键缺陷:Arena 数量上限 = 8 × CPU 核数,超出后线程需排队等待空闲 Arena,造成锁竞争瓶颈。且不同 Arena 的内存无法跨 Arena 归还,极易导致内存碎片累积。
tcmalloc — 三层缓存架构
tcmalloc 三层结构(Thread → Central → Page)
🧵 Thread Local Cache
每线程私有,无锁
size 8B
size 16B
size 32B
size 256KB
86 个 size class 的 FreeList
缓存不足时
从 Central 取
缓存过多时
归还 Central
🏛️ Central Cache
细粒度 spinlock
Span(页集合) Span
Transfer Batch 批量转移
📄 Page Heap
全局唯一,Mutex 保护
128 个 Span List
(按页数)
大对象(>256KB)直接在此分配
💡 设计亮点:小对象分配完全在 ThreadCache 内完成,零锁竞争。对象大小按 2 的幂次对齐,最多浪费 12.5% 空间但换来极高速度。
jemalloc — Arena + Chunk + Run 三层
jemalloc 内存架构(tcache → Arena → Chunk → Run)
🗂 tcache (Thread)
每线程,无锁
🗂 tcache (Thread)
每线程,无锁
▼ tcache miss → 访问 Arena
🏟 Arena 0
绑定 CPU 核,细粒度 mutex
Chunk (2MB)
run (small 8B×N)
run (small 16B×N)
run (large 4KB)
Chunk (2MB)
run ...
🏟 Arena 1
绑定 CPU 核,细粒度 mutex
Chunk (2MB)
run ...

Arena 数 = CPU 核数 × 4
▼ 大对象(>1MB)直接 mmap
🗺 mmap Large Extent(直接映射)
💡 设计亮点:Size Class 分 small(8B~14KB,约 48 档)/ large(14KB~1MB)/ huge(>1MB) 三类,每个 run 只存放同一 size class 的对象,碎片率远低于 ptmalloc。
🔄 核心工作流程(malloc / free)
ptmalloc malloc 流程
1
获取 Arena
线程尝试 trylock 上次用的 Arena;失败则轮询其他 Arena;全满则创建新 Arena(上限 8×cores)或阻塞等待。
2
尝试 fast bins
size ≤ 80B 时,直接从对应 fast bin 的单链表头部取出 chunk,速度最快(但不合并相邻空闲块)。
3
查 small/large bins
在 small bins 精确匹配,或在 large bins 找最小满足的 chunk(best-fit)。找到后切割并将剩余放入 unsorted bin。
4
整理 unsorted bin
将 unsorted bin 里的 chunk 分类到 small/large bins,同时检查是否有精确匹配。
5
从 top chunk / mmap 分配
切割 top chunk 满足请求;若 top chunk 不足则用 brk() / mmap() 扩展堆。

ptmalloc free 流程

1
放入 fast bin(小块)
≤80B 的 chunk 直接挂到 fast bin 链表头,不合并邻居。
2
合并邻居
检查前后相邻 chunk 是否空闲,若是则合并(coalesce),放入 unsorted bin。
3
归还 OS(可选)
若 top chunk 超过阈值(128KB),调用 sbrk(-n) / munmap() 归还给 OS。
tcmalloc malloc 流程
1
计算 size class
将请求大小向上取整到最近的 size class(共 86 档)。
2
ThreadCache 查找(无锁)
直接访问当前线程 ThreadCache::freelist_[cl],链表非空则弹出,全程无锁,纳秒级。
3
从 CentralCache 批量补充
ThreadCache 空时,向 CentralCache 申请一个 batch(批量),减少跨层访问次数。CentralCache 用细粒度 spinlock。
4
从 PageHeap 申请 Span
CentralCache 无可用 Span 时,向 PageHeap 申请 N 页(全局 Mutex)。
5
大对象直接 PageHeap
>256KB 的请求跳过 ThreadCache,直接在 PageHeap 查找合适的 Span,或 mmap 新页。

tcmalloc free 流程

1
归还 ThreadCache(无锁)
小对象直接放回本线程 freelist,无需任何锁
2
GC 超额缓存
ThreadCache 总占用 > 4MB 时,GC 机制将部分 freelist 归还 CentralCache(soft limit)。
3
Span 归还 PageHeap
CentralCache 某 Span 中所有对象都释放后,将该 Span 归还 PageHeap;PageHeap 合并相邻 Span。
jemalloc malloc 流程
1
分类请求大小
small(≤14KB)→ 走 tcache / run;
large(14KB~1MB)→ 走 Arena 直接分配;
huge(>1MB)→ 直接 mmap。
2
查 tcache(无锁)
先查线程私有 tcache 对应 size class 的 bin,命中则直接返回,无需访问 Arena。
3
选择 Arena
tcache miss 时,通过 thread → arena 映射找到绑定的 Arena(按 CPU 核数均匀分布),加细粒度 mutex。
4
在 run 中分配
Arena 找到对应 size class 的当前活跃 run(slab),在其 bitmap 中标记一个空闲 region 为已用。
5
新建 Chunk / run
当前 run 已满时,从 Chunk 切割新 run;Chunk 不足时向 OS 申请 2MB 对齐的新 Chunk。

jemalloc free 流程

1
放入 tcache(无锁)
小对象先放回 tcache bin,延迟实际 free 操作。
2
更新 run bitmap
tcache flush 或大对象 free 时,清除 run 内对应 region 的 bitmap 位。
3
Chunk 回收 / 归还 OS
整个 run 空闲 → 回收到 Arena chunk map;整个 Chunk 空闲 → madvise(DONTNEED) 或 munmap 归还 OS。
🔒 锁模型与并发策略
ptmalloc 锁模型
Thread 1
Thread 2
Thread 3
↓ 竞争
Arena A
全局 mutex
Arena B
全局 mutex
  • 每个 Arena 一把粗粒度 Mutex
  • 线程超出 Arena 数时需轮询等锁
  • 同一 Arena 内所有操作串行化
  • 高并发瓶颈严重
tcmalloc 锁模型
Thread 1
private cache
Thread 2
private cache
Thread 3
private cache
↑↓ 仅补充/归还时访问
CentralCache
per-class spinlock
PageHeap
全局 Mutex(低频)
  • ThreadCache 操作完全无锁
  • CentralCache 按 size class 分锁(细粒度 spinlock)
  • PageHeap 全局 Mutex,但访问频率极低
  • 高并发表现优秀
jemalloc 锁模型
Thread 1
tcache
Thread 2
tcache
Thread 3
tcache
↓ miss 时绑定到 Arena
Arena 0
细粒度 mutex
Arena 1
细粒度 mutex
  • tcache 操作完全无锁
  • Arena 数 = 4 × CPU 核数,均匀分散压力
  • Arena 内锁粒度更细(bin 级别)
  • 并发 + 碎片控制双优
🧩 内存碎片管理策略
ptmalloc 碎片问题

ptmalloc 碎片问题是三者中最严重的:

  • 跨 Arena 不可复用:Arena A 释放的内存无法被 Arena B 使用
  • fast bin 不合并:小块频繁分配释放形成外部碎片
  • 长生命周期对象:阻塞相邻小块合并,堆顶无法收缩
  • 内存膨胀场景:多线程 + 短生命周期对象是灾难
实际案例:某 C++ 服务切换 ptmalloc → jemalloc 后,RSS 从 12GB 降至 4GB,降幅约 67%。
tcmalloc 碎片控制

tcmalloc 优先追求速度,碎片控制属于中等水平:

  • 向上取整 size class:最多浪费 12.5% 内存(内部碎片)
  • Span 合并:PageHeap 会合并相邻空闲 Span
  • ThreadCache GC:定期回收过多的线程缓存对象
  • 激进归还 OS:可配置 MADV_FREEMADV_DONTNEED
内部碎片率稳定,但外部碎片在内存使用模式不均匀时也会出现。
jemalloc 碎片优化

jemalloc 是三者中碎片控制最精细的:

  • 极细 size class:约 48 档 small class,内部碎片 <4%
  • run 内 bitmap:同 size class 紧密排列,避免外部碎片
  • Chunk 对齐:2MB 对齐便于元数据定位,减少边界浪费
  • 主动清理:arena.purge 主动归还脏页
  • 背景线程:可开启 background_thread 异步清理
Redis 默认使用 jemalloc,内存碎片率通常控制在 1.0~1.5 之间(>1.5 视为碎片严重)。
📊 性能维度对比

以下对比为相对表现(满分 10),实际数值因负载模式差异较大,仅供参考。

🚀 单线程分配速度
ptmalloc
7.5
tcmalloc
9.5
jemalloc
8.8
⚡ 多线程并发速度
ptmalloc
4.5
tcmalloc
9.5
jemalloc
9.2
🧩 内存碎片控制
ptmalloc
4.0
tcmalloc
7.2
jemalloc
9.5
💾 内存利用率
ptmalloc
5.0
tcmalloc
7.5
jemalloc
9.2
📈 高并发吞吐量
ptmalloc
3.8
tcmalloc
9.6
jemalloc
9.0
⚙️ 可观测性 / 调试
ptmalloc
3.0
tcmalloc
7.0
jemalloc
9.8
🔌 集成便利性
ptmalloc
10
tcmalloc
7.5
jemalloc
8.0
⚖️ 全面对比表
特性 🔶 ptmalloc2 🟣 tcmalloc 🔵 jemalloc
并发单元 Arena(有数量上限) ThreadCache(无上限) tcache + Arena(CPU 数×4)
锁粒度 粗粒度 Mutex(Arena 级) 无锁 + 细粒度 Spinlock + 低频 Mutex 无锁(tcache)+ 细粒度 Mutex(Arena bin)
小对象分配 fast bins(LIFO 单链表) ThreadCache freelist(无锁链表) tcache bin(无锁)→ Arena run bitmap
size class 数量 约 32 档 bin 约 86 档(精细) 约 48+ 档(small/large/huge 三级)
大对象处理 large bins + mmap(>128KB) PageHeap Span(>256KB) Arena large bin(>14KB)/ mmap huge(>1MB)
内存碎片 严重(跨 Arena 不复用) 中等(内部碎片约 12.5%) 最优(<4% 内部碎片)
内存归还 OS sbrk 缩减 + munmap(延迟) madvise DONTNEED/FREE(可配置) madvise + 可选 background_thread 主动清理
多线程性能 差(锁争用严重) 极优(Google 内部基准 >ptmalloc 5x) 优(与 tcmalloc 接近)
内存占用 高(碎片 + Arena 开销) 中(每线程缓存固定开销) 低(精细管理)
可观测性 mtrace、mallinfo(有限) pprof CPU/内存 profiling mallctl API、统计 epoch、内置 profiling
线程缓存 GC 有(soft/hard limit,动态调整) 有(tcache GC + arena background purge)
适合语言/运行时 C/C++(Linux 默认) C/C++、Go(内置变体)、Java(可替换) C/C++、Rust(默认)、Erlang、PHP
知名用户 Linux 系统级程序 Chrome、Google 搜索、Go runtime Firefox、Redis、FreeBSD、Facebook
🎯 适用场景与选型建议
何时用 ptmalloc
  • 无特殊需求的 Linux C/C++ 程序(零配置)
  • 单线程或极低并发的工具/脚本
  • 已验证内存行为稳定、无碎片问题的遗留系统
  • 容器化部署且内存充足、不关心用量优化
⚠️ 避免场景:高并发服务端、长期运行后内存不归还、大量小对象频繁分配释放。
何时用 tcmalloc
  • 追求极致分配吞吐量(如 Google 级别高 QPS 服务)
  • Go 语言(已内置,无需手动替换)
  • 需要 pprof 内存/CPU profiling 的项目
  • 大量短生命周期小对象(<256KB)场景
  • CPU 核数多、线程数多的场景
💡 替换方式:LD_PRELOAD=/usr/lib/libtcmalloc.so ./app 或编译时链接 -ltcmalloc
何时用 jemalloc
  • 内存占用敏感的长期运行服务(如数据库、缓存)
  • Redis 生产环境(官方推荐)
  • Rust 语言(cargo 默认使用 jemalloc)
  • 需要精细内存监控和调优的场景
  • 内存碎片问题已影响稳定性的服务
  • 高并发 + 内存效率兼顾的场景
💡 替换方式:LD_PRELOAD=/usr/lib/libjemalloc.so ./app,Redis 可在编译时 make USE_JEMALLOC=yes
🧭 快速选型决策树
你的程序需要高并发?
ptmalloc
系统默认,够用了
内存占用/碎片是首要问题?
否(更关注速度)
tcmalloc
最快分配速度
是(内存优先)
jemalloc
最低碎片率