本文来自 jasone.github.io/2025/06/12/…
👉️👉️公众号 猩猩程序员 首发,欢迎关注
jemalloc 内存分配器最初构想于 2004 年初,并已公开使用近 20 年。由于开源软件许可证的性质,jemalloc 将无限期地保持公开可用状态。但其上游的活跃开发已经终止。本文简要回顾了 jemalloc 的各个开发阶段,并总结了每个阶段的一些成功和失败,最后附上一些回顾性评论。
阶段 0:Lyken
2004 年,我开始着手开发 Lyken 编程语言,用于科学计算。虽然 Lyken 最终成为一个死胡同,但其手动内存分配器在 2005 年 5 月前就已功能完备(而本应利用其特性的垃圾回收器却从未完成)。2005 年 9 月,我开始将该分配器集成进 FreeBSD,到了 2006 年 3 月,我将其从 Lyken 中移除,改用一些对系统分配器功能的薄封装。
为什么在投入这么多精力后又把分配器从 Lyken 中移除?因为当它被集成进 FreeBSD 后,很快就显现出系统分配器唯一缺失的功能只是一个可以按线程追踪分配量、从而触发垃圾回收的机制。而这个功能完全可以用线程局部存储和 dlsym(3) 来通过薄封装实现。有趣的是,几年后 jemalloc 确实添加了 Lyken 当初所需的统计功能。
阶段 1:FreeBSD
2005 年,当多核处理器逐渐普及时,FreeBSD 使用的是 Poul-Henning Kamp 编写的出色的 phkmalloc 分配器,但它并不支持多线程并行执行。Lyken 的分配器看起来是一个显而易见的扩展方向。在朋友和同事的鼓励下,我将这个分配器整合进了 FreeBSD,它很快就被称作 jemalloc。
但问题来了:整合完成后很快发现,jemalloc 在某些负载下会产生严重的内存碎片问题,尤其是在 KDE 应用程序诱发的场景下。正当我以为差不多完成时,这一现实问题严重质疑了 jemalloc 的可行性。
简而言之,碎片问题是因为使用了统一的区段分配策略(即没有按大小分类)。我虽然借鉴了 Doug Lea 的 dlmalloc,但缺乏后者经过多年实战考验的复杂启发式算法。这引发了一系列紧张的研究与实验。到 jemalloc 随 FreeBSD 一起发布时,它的内存布局算法已经完全重写,采用了按大小分区的策略,正如 2006 年 BSDCan 上的 jemalloc 论文中所描述的那样。
阶段 1.5:Firefox
2007 年 11 月,Mozilla Firefox 3 接近发布,但其内存碎片问题仍未解决,尤其是在 Windows 上。因此,我开始与 Mozilla 合作优化内存分配器。将 jemalloc 移植到 Linux 上几乎是轻而易举的事,但 Windows 上却问题重重。
当时 jemalloc 的源代码还在 FreeBSD 的 libc 中,因此我们实际上对 jemalloc 进行了分叉开发,添加可移植性代码,并将与 FreeBSD 相关的改动回馈上游。整个实现仍然只有一个文件,这使得分叉维护尚可接受,但实现复杂度此时早已超出单文件所能承载的合理范围。
多年后,Mozilla 开发者尝试回归上游 jemalloc,并做出了许多贡献。但可惜的是,Mozilla 的基准测试始终显示他们分叉的版本性能优于上游版本。我不清楚这是过度优化导致的局部最优,还是性能真的退化了,但这始终是我对 jemalloc 最大的遗憾之一。
阶段 2:Facebook
2009 年我加入 Facebook 后,惊讶地发现:Facebook 基础设施中 jemalloc 推广的最大障碍竟是缺乏调试工具支持。一些关键服务依赖 jemalloc 控制内存碎片,但工程师们却只能用 tcmalloc 和 gperftools 的 pprof 工具排查内存泄露。
于是,在 jemalloc 1.0.0 版本中,我加入了与 pprof 兼容的堆分析功能。
接下来的几年里,jemalloc 迁移到 GitHub,继续零散开发,社区贡献逐渐增多:
- 3.0.0:引入了全面的测试基础设施和 Valgrind 支持
- 4.x 系列:引入了基于衰减的内存回收机制、JSON 格式遥测数据
- 5.x 系列:从“chunk”转向“extent”,为支持 2MiB 的大页做准备
争议最大的是我在 5.0.0 版本中移除了 Valgrind 支持,因为它维护起来复杂(有很多微妙依赖),而 Facebook 内部并不使用,主要依赖 pprof 和 MemorySanitizer。我几乎没有收到任何相关反馈,于是误以为没人用它。后来才知道,Rust 编译出来的程序直接集成了 jemalloc,Rust 社区中很多人也用 Valgrind。很多人因此非常愤怒。这也导致 jemalloc 早早被移出 Rust 二进制中。
Facebook 的内部遥测系统极其强大,让我们能在千变万化的服务中观察性能趋势,尤其对内存分配器的开发极为有利。比如:
- 快路径优化在拥有
perf汇总数据的情况下更容易调优 - 避免碎片仍然是难题,但若几千种工作负载表现都良好,那说明更改是安全的
此外,jemalloc 本身的统计系统正是受 Facebook 监控文化的启发而添加的,后来也帮助了无数其他应用调优和调试。
我在 Facebook 最后一年的时候,还被鼓励组建了一个小型 jemalloc 团队,解决一些原本不可能单人完成的难题,推进了性能改进、持续集成测试和遥测系统。在我 2017 年离职后,jemalloc 团队继续在我几乎不参与的情况下,由优秀的同事 Qi Wang 领导,完成了数年高质量的开发与维护。
阶段 3:Meta
Facebook 更名为 Meta 后,jemalloc 的开发方向开始发生变化。基础架构团队减少了对底层技术的投入,更加关注投资回报。从 commit 记录上就能看出变化。
例如,“大页分配”(HPA)早在 2016 年就种下了种子,但开发进度时快时慢,后来补丁叠加太多,缺乏重构,代码健康状况持续恶化。最终该功能发展停滞。
这让我很遗憾,但我已经多年没有深度参与。如今,Meta 内部已经没人关注 jemalloc 的长期通用性发展。
我不想渲染戏剧冲突,但可以说,即便大多数人都在尽力而为,jemalloc 最终在 Meta 手中走向终结也是令人唏嘘的。公司文化会随着外部与内部压力而演化。个人往往被逼到三种选择:1)在巨大压力下做出不当决策;2)在压力下妥协;3)被绕过。我们偶尔能延缓这种系统性崩溃,甚至激发一时的复兴,但无法阻止整体趋势。
我仍然对以前所有在 jemalloc 上共事的同仁,以及 Facebook/Meta 长期以来对该项目的支持表示深深的感激。
阶段 4:停滞
那么现在呢?对我来说,jemalloc 的“上游”开发已经结束。Meta 的需求早已与外部用户需求脱节,他们也应该走自己的路了。
如果我要重启开发,第一步就需要几百小时的重构来偿还技术债。而我对后续工作的兴趣不足以支撑这样的前期投入。也许未来有人会基于 dev 分支或已经三年未更新的 5.3.0 版本创建新的分支项目。
上文提到了一些阶段性失败,但也有一些更普遍、让我这个做开源多年的人都感到意外的失败。
- 移除 Valgrind 支持带来负面反馈的根本原因是:对外部用户需求缺乏了解。如果我早知道 Valgrind 很重要,我肯定会和其他人一起保留它。
- 例如,我曾完全不知道 Android 用了 jemalloc 做默认分配器长达两年,也是在它被替换后才得知此事。
尽管 jemalloc 一直是公开开发的(没有被 Facebook 完全封闭),但它从未形成跨组织的主要贡献者团队:
- Mozilla 开发者 Mike Hommey 推动 Firefox 回归上游几乎成功,却最终未能达成
- CMake 构建系统的迁移尝试反复失败,从未完成
我曾在 Darwin 平台的惨痛经历中学到,封闭式的开源项目无法茁壮成长(HHVM 项目再次印证了这一点)。jemalloc 即便是开放开发,也仍然缺乏独立成长所需的外部结构和资源。
对我而言,jemalloc 算是个奇怪的转折,因为我本人25 年来一直主张垃圾回收优于手动内存管理。如今我回归了垃圾回收系统,感到非常开心。
但 jemalloc 是个极其充实且有意义的项目。感谢所有让这个项目变得有价值的人:合作者、支持者,以及使用者。