jemalloc为何重要:大规模内存分配
jemalloc支撑着全球最大系统之一的内存分配。它如何减少碎片,以及Meta为何持续加码投入。

每次你的程序调用 malloc(),都得决定把虚拟内存中的哪一块交出去。这个决定对小程序来说微不足道,但到了大规模场景就成了棘手的工程问题。糟糕的分配器会造成内存碎片、浪费 RAM、在线程之间制造锁竞争,还会在操作系统回收页面时引发延迟尖峰。好的分配器则能避免这些问题。jemalloc 就是一个好的分配器,而 Meta 刚刚再次表态继续押注它,因为在他们的规模下,好分配器和普通分配器之间的差距就是数十亿美元的硬件成本。
大多数开发者从不考虑内存分配。调用 new 或 malloc,拿到一个指针就完事了。但在 Meta 的规模下,每天数十亿次请求、数百万台服务器、PB 级的 RAM,分配器的行为会直接、可衡量地影响硬件效率、尾延迟和运维成本。
内存分配器到底做了什么
内存分配器位于你的程序和操作系统之间。操作系统以大块(页,通常是 4KB 或 2MB)提供内存,而你的程序需要的是小而大小不一的内存片段(这里一个 24 字节的字符串,那里一个 4096 字节的缓冲区)。分配器的工作就是把操作系统给的页切分成程序所需的块,并回收已释放的块供后续分配使用。
朴素的做法是每次分配都向操作系统申请一个新页,释放时再还回去,这慢得离谱。系统调用有开销,而页通常比大多数分配请求大得多。为了存一个 24 字节的字符串,你却要占用 4KB 内存。
真正的分配器会维护一个内存池,从中做子分配。挑战在于:尽量减少碎片(分配之间那些小到无法使用的空隙)、尽量减少锁竞争(多个线程同时分配),以及尽量降低开销(每次分配的元数据相对于分配本身要足够小)。
jemalloc 的工作原理
jemalloc 由 Jason Evans 开发(「je」即来源于此),最初为 FreeBSD 开发,后来被 Meta(当时还叫 Facebook)采用为 C 和 C++ 服务的默认分配器。它的设计通过一些具体技术应对上述三大挑战。
线程缓存消除竞争。每个线程都有自己的小对象缓存。线程调用 malloc() 分配小对象时,分配完全由线程本地缓存完成,没有锁,没有原子操作,也没有竞争。只有当线程缓存耗尽时,才会去共享的 arena 补充。
jemalloc allocation flow:
Thread calls malloc(64)
↓
Check thread cache for 64-byte size class
→ Hit: return cached chunk (no lock, no contention)
→ Miss: refill from arena
↓
Arena (shared, but per-thread affinity)
→ Find a partially-full slab for 64-byte objects
→ Carve out a chunk
→ Return to thread cache
↓
Return chunk to caller
Thread caches handle 95%+ of allocations without any locking.
大小类减少碎片。jemalloc 不会严格按照请求的字节数分配,而是向上取整到最接近的大小类。大小类经过精心挑选:8、16、32、48、64、80、96、112、128、160、192、224、256,依此类推,且随着尺寸增大,间隔也逐渐变大。这意味着一个 50 字节的分配会拿到 64 字节的块(浪费 23%),听起来不太好,但实际上很合理,因为同一大小类的所有块都可以互换,所以在一个大小类内部不存在外部碎片。
Slab 把同尺寸的分配归拢在一起。每个 slab 是一段连续内存,被划分为同一大小类的块。用于 64 字节对象的 slab 里只有 64 字节的块。这消除了最糟糕的一类碎片:已释放的内存因为夹在不同大小的活动分配之间而无法被重用。
内存碎片问题
内存碎片是长期运行服务的无声杀手。刚启动的服务内存利用率很高,分配紧密排列。经过几天或几周的混合分配和释放,堆就变成了瑞士奶酪:活动分配之间布满了许多小的空闲缝隙。空闲内存总量可能有 2GB,但最大的连续空闲区域可能只有 64KB。
外部碎片(分配之间无法使用的缝隙)和内部碎片(取整导致分配内部的浪费)都很重要,但外部碎片更严重。内部碎片受限于大小类的间隔,最坏情况下每次分配浪费约 25%。而外部碎片没有上限,并且会随时间不断增长。
jemalloc 基于 slab 的方案基本消除了小对象分配(也就是绝大多数分配)的外部碎片。由于 slab 中的所有对象尺寸相同,释放一个对象产生的空洞,恰好适合同一大小类的下一次分配。不存在无法利用的缝隙。
对于大对象分配(通常大于 14KB),jemalloc 采用不同的策略:大对象直接占用独立的页,释放的页可以归还给操作系统,或者重新用于其他大小类。这里仍然可能产生碎片,但大对象分配相对少见。
jemalloc、glibc malloc 与 tcmalloc 对比
Linux 生态中的三大主流分配器各有不同的取舍。
- glibc malloc(ptmalloc2)是大多数 Linux 系统的默认分配器。它使用 arena 来实现线程扩展性,但大小类比 jemalloc 少,因此在长期运行的服务中会产生更多碎片。它的主要优势在于是默认选项,无需任何配置。
- tcmalloc(Google)是线程缓存型的 malloc,最初为 Google 的 C++ 服务开发。它的线程本地缓存非常出色,开销也低。它适合大量短生命周期分配的工作负载,而对于内存压力大、碎片问题突出的场景则不太合适。
- jemalloc针对持续负载下的低碎片和可预测行为进行了优化。它使用比其他分配器更精细的大小类和 slab 管理。代价是每次分配的开销略高(元数据更多),以换取长期更好的内存利用率。
选择哪个取决于你的工作负载。对于短生命周期的进程,三者表现差不多。对于混合分配模式的长期运行服务(Web 服务器、数据库、缓存),jemalloc 的抗碎片能力通常更胜一筹:相同负载下,相比 glibc malloc 可以节省 10%–30% 的 RAM,而在大规模场景下,这直接转化为硬件成本的节省。
Meta 为什么在意
在 Meta 的规模下,C++ 服务内存使用量降低 10%,每月节省的硬件成本就是数百万美元。注意,是每月,不是每年。当你运营着数百万台服务器,每台上的服务每天分配和释放内存数十亿次,分配器的效率就是预算表上一个实打实的条目。
Meta 对 jemalloc 的持续投入主要集中在几个方面:更好的大页支持(减少大内存系统上的 TLB 缺失)、改进内存归还操作系统的机制(负载下降时降低常驻内存),以及更好的性能分析工具(弄清内存用在哪里、又在哪里被浪费)。
其中性能分析尤其有意思。jemalloc 内置了堆分析功能,你可以让它对分配进行采样,生成一份剖析报告,展示内存在哪里被分配、有多少处于活动状态或已释放,以及堆的碎片化程度。这种分析在生产环境中的开销几乎可以忽略,因此可以在生产服务器上持续运行。
在你的项目中使用 jemalloc
对于 C/C++ 程序,切换到 jemalloc 通常非常简单。在 Linux 上,你可以直接链接它,或者使用 LD_PRELOAD 在运行时注入,无需修改任何代码。
# Install jemalloc
sudo apt install libjemalloc-dev # Debian/Ubuntu
brew install jemalloc # macOS
# Use LD_PRELOAD — works with any program, no recompilation
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so ./my_server
# Or link at compile time
gcc -o my_server my_server.c -ljemalloc
# Enable profiling
export MALLOC_CONF="prof:true,prof_prefix:jeprof"
./my_server
# Then analyze: jeprof --svg ./my_server jeprof.*.heap > heap.svg
一些知名项目默认使用 jemalloc:Redis、Rust(在某些平台上默认使用 jemalloc 作为分配器)、Firefox(jemalloc 最初为 FreeBSD 开发,而 Firefox 当时也以 FreeBSD 为目标平台之一),以及许多游戏引擎。
对于托管内存的语言(Python、Java、Go),由语言运行时负责分配,jemalloc 并不直接适用。但线程本地缓存、大小类、slab 分配这些概念,几乎出现在每个现代运行时的垃圾回收器和内存分配器中。以 Go 运行时为例,其分配器采用了受 tcmalloc 启发的设计,使用每个 P(处理器)的缓存和大小类。
看不见的基础设施
内存分配是一种基础设施:一切正常时无人察觉,出问题时则是灾难。一个因碎片而逐渐泄漏内存的服务,最终会被 OOM 杀掉,而原因不会出现在任何应用日志中,因为它位于应用层之下。
对大多数应用来说,默认分配器已经够用。但如果你运行的是长期服务,遇到无法解释的内存增长,或者运行在硬件效率至关重要的规模上,那么了解你的分配器,并可能换一个更好的,就是你能做出的杠杆率最高的基础设施改动之一。jemalloc 没有什么魔法,它是工程:精心的数据结构设计、有依据的权衡取舍,以及对大规模场景下关键细节的持续专注。


