把 jemalloc 和 tcmalloc 放在一起看,真正值得关心的不是“谁更快”这类口号,而是它们分别把锁竞争、碎片、回收压力和调优复杂度放在了哪里。tcmalloc 的主线是:把最热的分配/释放路径尽量压进 per-CPU cache,用极少同步换多核扩展性;jemalloc 的主线是:把 arena、slab、extent、decay、profiling 组织成一套可预测、可观测、可调的内存管理体系。
如果应用是典型的长生命周期多线程服务,这个区别会直接反映到三个工程问题上:吞吐和尾延迟、RSS 与碎片、以及线上排障手段。本文只聚焦 jemalloc 与 tcmalloc,不展开 glibc ptmalloc、mimalloc 等其他路线。
一、先看图
flowchart LR
subgraph T["tcmalloc"]
T1[线程/CPU 上的 malloc/free] --> T2[Front-end<br/>per-CPU cache<br/>旧模式:per-thread cache]
T2 -->|miss / overflow| T3[Middle-end<br/>Transfer Cache]
T3 --> T4[CentralFreeList<br/>按 size class 管理]
T4 --> T5[Back-end<br/>PageHeap / Hugepage-aware PageHeap]
T5 --> T6[mmap / hugepage / OS]
end
subgraph J["jemalloc"]
J1[线程上的 malloc/free] --> J2[tcache<br/>thread-specific cache]
J2 -->|miss / flush| J3[arena]
J3 --> J4[bin + slab<br/>small objects]
J3 --> J5[extent<br/>large objects]
J3 -. decay / purge .-> J6[madvise / OS]
J4 --> J6
J5 --> J6
end
这张图已经把两者的气质差异画出来了:
- tcmalloc 更像三段流水线:前端 cache 抗住热路径,中端负责批量搬运,后端负责页级与 hugepage 级管理。
- jemalloc 更像多 arena 的内存治理器:线程先吃本地 tcache,miss 以后回到 arena,再由 arena 管小对象 slab、大对象 extent,以及回收节奏。
二、共同地基:现代分配器到底在解决什么
jemalloc 和 tcmalloc 都不是在“优化一次
malloc()
调用”这么简单。它们共同要解决四个老问题:
- 小对象分配太频繁,不能每次都拿锁。
- 不同大小的对象混在一起会形成碎片。
- free 之后不等于 RSS 立刻下降。
- 多线程程序里,分配路径和释放路径常常不在同一个线程。
所以两者都用了三类共通手段:
| 共同手段 | 作用 |
|---|---|
| size class | 把任意大小请求映射到有限档位,避免每次做通用分割 |
| 本地 cache | 让绝大多数小对象分配/释放走无锁或低锁快路径 |
| 页级回收 | 在对象层之上再做页/extent/span 级合并与返还 |
分歧不在“要不要这些东西”,而在本地 cache 是按线程还是按 CPU,中央结构按什么粒度组织,回收时优先追求吞吐、碎片边界还是 hugepage 完整性。
三、tcmalloc:把热路径推进 per-CPU
3.1 三层结构是它的骨架
tcmalloc 官方设计文档把自己拆成三部分:
- Front-end:给应用直接提供对象,目标是快。
- Middle-end:给前端批量补货或回收对象,主要组件是
TransferCache和CentralFreeList。 - Back-end:向 OS 取内存、管理页、回收大块内存。
这种分层很重要,因为它意味着 tcmalloc 的核心优化思路不是“把全局结构做得更聪明”,而是尽量不碰全局结构。
3.2 真正的关键:per-CPU cache
现代 tcmalloc 的默认快路径是 per-CPU mode,而不是早年的 per-thread mode。每个逻辑 CPU 都有自己的 cache 区域,里面按 size class 维护空闲对象指针数组。小对象分配时,如果本 CPU 的对应 size class 还有空闲对象,直接取走;释放时,如果本 CPU 缓存还没满,直接放回。
这条路径的关键不是“每 CPU 一份数据”本身,而是它在 Linux 上借助 restartable sequences(rseq) 实现更新:在最热的 push/pop 路径里,不需要原子指令,也不需要互斥锁;如果线程在临界片段中被抢占或迁移,序列会从头重启。
工程上可以把它理解成:
- 常态:线程留在当前 CPU,分配/释放只动本 CPU 的小块缓存,极便宜。
- 非常态:缓存空了或满了,才批量找 middle-end。
这就是 tcmalloc 在高并发小对象场景里经常表现很强的原因:它把“同步”从每次对象操作,降成了偶发的批量 refill / drain。
3.3 Middle-end 解决“跨 CPU 流动”
如果一个对象在 CPU 3 分配、在 CPU 11 释放,只靠 per-CPU cache 很容易把内存困死在错误的位置。tcmalloc 为此引入了 middle-end:
- Transfer Cache:适合在不同 CPU 之间快速搬运一批对象。
- CentralFreeList:按 size class 管理 spans,前端需要补货时从这里拿,前端回收溢出时往这里还。
这层通常带锁,但因为是批量操作,锁成本被摊薄了。tcmalloc 的思路很明确:把锁留在批处理层,不要让锁进入每次对象分配。
3.4 Back-end 不只是在“拿页”,还在管 hugepage
tcmalloc 后端历史上是 pageheap;现代实现又增加了 hugepage-aware pageheap。官方 Temeraire 设计文档说明得很直白:它的目标之一,就是减少 pageheap 内部碎片,并提高 hugepage 保持完整的概率。
这条路线的含义是:
- 对于普通小对象,前端和中端决定快不快;
- 对于大堆、高 RSS、TLB 敏感的服务,后端如何维护 hugepage 完整性会变得很重要。
tcmalloc 还把自己的逻辑页大小做成了编译期选项(4 KiB、8 KiB、32 KiB、256 KiB)。这也是很典型的 tcmalloc 风格:把页级布局直接作为性能/空间旋钮暴露出来。
3.5 tcmalloc 的代价
tcmalloc 不是白拿收益,它主要有三类代价:
- per-CPU cache 的占用随 CPU 数增长。 官方文档明确指出,机器 CPU 越多,可缓存的总内存也越多。
- 页大小与 release 策略没有免费午餐。 更大的逻辑页有利于聚集和 TLB,但也更容易形成“页面里只剩少量活对象”的悬挂空间。
- 更激进地释放内存会破坏 hugepage。 官方
tuning 文档明确提醒,过于积极地
ReleaseMemoryToSystem会增加回填 fault,也会打碎 hugepage。
所以 tcmalloc 并不是“只看吞吐”的分配器;更准确地说,它是先把吞吐和多核扩展性做到极强,再通过 pageheap / hugepage 策略去把空间问题尽量补回来。
四、jemalloc:把碎片与回收做成一等问题
4.1 多 arena + tcache 是它的基本盘
jemalloc 的官方 README 把目标写得很清楚:强调 fragmentation avoidance 和 scalable concurrency support。它当然也在做并发扩展,但切入点和 tcmalloc 不一样。
jemalloc 的实现笔记里明确写了两件事:
- 用 multiple arenas 降低多线程程序里的锁争抢;
- 用 thread-specific caching(tcache) 让大多数请求完全避开同步。
默认情况下,jemalloc 自动 arena 数是 CPU 数的 4 倍。这能有效降低竞争,但文档也同时强调了代价:每个 arena 有固定开销,而且 arena 彼此独立,会带来少量额外碎片。
这很能体现 jemalloc 的风格:它愿意接受可量化、可解释的额外结构成本,换取更稳定的并发和更可控的碎片行为。
4.2 jemalloc 的核心对象:bin、slab、extent
jemalloc 把内存概念化为 extents。在这个基础上:
- small objects:按 size class 分组,落在 slab 里;
- large objects:每个对象由自己的 extent 支撑。
small object 的分配方式尤其关键。jemalloc 的文档说明:
- slab 内部用 bitmap 跟踪哪些 region 已占用;
- size class 的间距设计为“每次翻倍大致分 4 档”,使大多数 size class 的内部碎片控制在约 20%;
- small size class 小于 4 × page size,large size class 从这个边界往上延伸。
也就是说,jemalloc 的重点不是把 size class 做得“非常少”,而是把它做成一套更细密、更强调空间边界的分级体系。
4.3 jemalloc 真正拉开差距的地方:回收与治理
如果只看“有 arena、有 tcache、有 size class”,jemalloc 和 tcmalloc 好像只是术语不同。真正的分野在回收治理。
jemalloc 把这部分做成了一整套可调策略:
dirty_decay_ms:控制脏页从“空闲但仍占物理内存”到被 purge / reuse 的节奏。muzzy_decay_ms:控制已经做过 lazy purge 的页何时进一步清理。background_thread:后台线程按需异步做 purge。oversize_threshold:默认把超过阈值(默认 8 MiB)的请求放进专门 arena,避免和小对象混住。retain:是否保留不用的虚拟地址空间以便后续复用。percpu_arena:允许按 CPU 动态绑定 arena,但默认关闭。
这些选项的共同指向非常明确:jemalloc 不满足于“能分配得快”,它希望把 RSS、返还节奏、oversize 隔离、虚拟地址复用都纳入同一套 mallctl 治理面。
4.4 可观测性是 jemalloc 的强项
jemalloc 在 2010 年以后把 heap profiling、统计和
mallctl*()
接口都做得很完整。对线上服务来说,这意味着两件事:
- allocator 内部状态本身是可读的。
- 调优动作可以比较精细地做,而不是只能换一个库重跑。
如果团队经常要回答“RSS 为什么没掉”“到底是 tcache、arena 还是大对象导致的”“哪类分配在拖高堆占用”这类问题,jemalloc 的工具化程度通常更顺手。
4.5 jemalloc 的代价
jemalloc 的代价也很明确:
- tcache 以线程为单位,会把一部分对象暂存在各线程本地。 文档明确指出,这会增加内存占用与碎片。
- arena 不是免费的。 arena 越多,竞争越低,但固定开销与独立管理导致的碎片也会上去。
- 调优面更大,意味着理解成本更高。 decay、background thread、retain、oversize、extent 行为都可能影响结果。
所以 jemalloc 的优势不在“默认一定最优”,而在当你真的在乎碎片边界、返还节奏和可观测性时,它给你的抓手更多。
五、并排对比:它们到底差在哪
| 维度 | tcmalloc | jemalloc |
|---|---|---|
| 热路径目标 | 把绝大多数小对象操作压进 per-CPU cache | 用 tcache 避开同步,再由 arena 承接 miss |
| 本地 cache 归属 | 现代默认按 CPU;旧模式按线程 | 主要按线程 |
| 同步策略 | 快路径尽量不用锁,锁留给批量 refill / drain | arena + bin 降低争用,快路径靠 tcache |
| 中央结构 | Transfer Cache + CentralFreeList + PageHeap | 多 arena;每个 arena 管 bin/slab/extent |
| 大对象管理 | 大于 kMaxSize 直接走 back-end |
large object 由独立 extent 支撑;oversize 可进专门 arena |
| hugepage 取向 | 很强,尤其是 hugepage-aware pageheap / Temeraire | 有相关空间复用与 purge 策略,但设计重心不在 hugepage |
| 碎片治理方式 | 靠 size class、pageheap、release、hugepage 布局来压 | 把 size class、oversize 隔离、decay、extent 复用做成系统化策略 |
| 可观测性 | GetStats()、sampling、realized
fragmentation |
mallctl*()、malloc_stats_print()、heap
profiling |
| 典型风险 | 高 CPU 数下 cache 占用变大;过快 release 破坏 hugepage | 高线程数下 tcache 占用增大;arena 过多会带来额外碎片 |
有一个很重要的判断:tcmalloc 更像“把 allocator 做成 CPU 本地化的数据面”,jemalloc 更像“把 allocator 做成可调的内存控制面”。
六、适用场景
6.1 更偏向 tcmalloc 的场景
下面这些情况,通常值得优先看 tcmalloc:
- 小对象非常频繁,且多核并发很高。
- 线程数远大于 CPU 数,希望本地缓存更多随 CPU 而不是随线程增长。
- 堆很大,且对 hugepage / TLB 敏感。
- 更关心热路径分配成本,而不是 allocator 级的精细治理。
典型关键词是:高 QPS 服务、对象生命周期短、跨核竞争重、CPU 数多、堆占用大。
6.2 更偏向 jemalloc 的场景
下面这些情况,通常更适合先看 jemalloc:
- 服务是长生命周期进程,最痛的问题是 RSS 膨胀、碎片和返还节奏。
- 需要把大对象和小对象分开治理。
- 希望 allocator 自身提供更强的 introspection / profiling。
- 愿意花一些时间做
mallctl级别调优。
典型关键词是:常驻进程、内存曲线波动大、线上经常要解释 RSS、希望把“调 allocator”当成一个正式运维手段。
6.3 两者都不该替你做的事
无论选哪个分配器,它们都不能替代下面这些工作:
- 对象生命周期设计。
- 业务层对象池 / region allocator / arena allocator。
- 峰值内存容量规划。
tcmalloc 官方文档明确提醒:释放速率只能让峰值之后的 footprint 回落,不能把峰值本身变小。这条对 jemalloc 也成立——allocator 能缓解碎片与回收抖动,但不能消灭业务层真实的峰值需求。
七、三个常见误判
7.1 “free 了,RSS 怎么还不掉?”
这通常不是 allocator 坏了,而是:
- 对象虽然 free 了,但页还没形成可返还的完整空闲区;
- 空闲内存还在本地 cache、central cache、arena 或 pageheap 里等复用;
- 系统使用的是 lazy purge /
MADV_FREE,物理页不会立刻显著下降。
7.2 “切到更激进的 release,就一定更省内存”
不对。tcmalloc 的 tuning 文档明确指出,激进 release 会增加后续 fault 成本,还会打碎 hugepage。jemalloc 的 decay 也是同一个道理:回收更快,不代表全局更优,只是把空间换成了未来重用成本与抖动。
7.3 “分配器选型只看 benchmark 排行”
这也是误区。用户态分配器最怕把单机 microbenchmark 当成全部真相,因为线上真正决定体验的往往是:
- 峰值后能不能回落;
- 跨线程/跨 CPU 释放多不多;
- 线程数与 CPU 数的比例;
- 是否依赖 hugepage;
- 是否需要 allocator 自带分析手段。
八、小结
- tcmalloc 的核心是把热路径推进 per-CPU,用 rseq 把同步成本压到极低,再用 middle-end 和 hugepage-aware back-end 收拾全局问题。
- jemalloc 的核心是把 arena、slab、extent、decay、profiling 串成完整体系,目标是并发可扩展、碎片可控、治理可观测。
- 如果更在乎极热路径和多核扩展性,先看 tcmalloc。
- 如果更在乎RSS 曲线、碎片边界和线上调优抓手,先看 jemalloc。
- 真正成熟的选型方式不是问“谁更快”,而是问:我的负载到底更怕锁、怕碎片、怕 hugepage 被打碎,还是怕线上看不见 allocator 在干什么。
参考资料
官方文档 / 源码
- jemalloc README
- jemalloc 5.x
jemalloc.3man page - Google TCMalloc
docs/design.md - Google TCMalloc
docs/rseq.md - Google TCMalloc
docs/tuning.md - Google TCMalloc
docs/stats.md - Google TCMalloc
docs/temeraire.md
设计说明 / 文章
- Jason Evans, Scalable memory allocation using jemalloc, Facebook Engineering, 2011
- Beyond malloc efficiency to fleet efficiency: a hugepage-aware memory allocator, OSDI 2021
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】HugeTLB 与 THP
大页能让一条 TLB 覆盖 2MB 乃至 1GB,但 THP 为什么在数据库里默认关掉?本文讲 HugeTLB 预留池、THP 的 khugepaged、defrag stall、madvise 模式、file-backed THP、以及工程上的取舍。
内存分配器擂台:jemalloc vs tcmalloc vs mimalloc vs glibc
分配器的 micro-benchmark 全是骗人的。真正的差距在碎片率和尾延迟。我们把四个分配器塞进一个真实的 HTTP 服务器,跑 24 小时,看谁先崩。
【操作系统百科】内核内存调试
内核内存 bug 是最难追的:UAF、OOB、double free、leak 都可能沉默数月。本文讲 KASAN 三种模式、KFENCE 生产采样、kmemleak、SLUB_DEBUG、UBSAN/KCSAN 联动。
【操作系统百科】VFS 四层抽象
Linux 的一切皆文件靠 VFS 实现——superblock、inode、dentry、file 四层抽象加 ops 表。本文讲 VFS 核心数据结构、dcache、inode cache、RCU lookup,以及文件系统如何插入 VFS。