13、内存分配器原理:ptmalloc、tcmalloc、jemalloc简介

说到C语言的内存管理,很多人第一反应就是mallocfree。但你想过没有,这两个函数背后到底是谁在干活?

说白了,真正管内存的,是内存分配器。我这些年做嵌入式和高并发服务,跟这三个分配器打过不少交道——ptmalloc、tcmalloc、jemalloc。每个都有自己的脾气。

13.1 为什么需要内存分配器?

操作系统提供了brkmmap来分配内存,但这两个是系统调用,每次调用都要陷入内核。你想想看,如果每次malloc都触发一次系统调用,那性能得多差?

所以内存分配器的作用就是:提前从系统申请一大块内存,然后自己管理,按需分给应用程序。这样大部分mallocfree都在用户态完成,速度飞快。

核心目标:

  • 减少系统调用次数
  • 减少内存碎片
  • 提高分配和释放的速度
  • 多线程环境下尽量不互相干扰

13.2 ptmalloc——Linux的默认分配器

ptmalloc是glibc自带的分配器,也是大多数Linux系统默认使用的。我刚开始做C开发时,用的就是它,也没觉得有什么问题——直到项目上了多线程。

13.2.1 核心设计

ptmalloc基于一个叫arena的概念。每个arena管理一大块内存,内部再用bin来分类管理不同大小的空闲块。

  • 主arena:主线程独享,用brk扩展堆
  • 非主arena:子线程使用,用mmap分配
  • bin分类:fast bins、small bins、large bins、unsorted bin

分配时,ptmalloc会先查fast bins,再查small bins,最后才去large bins和unsorted bin里找。如果都找不到,就向系统申请新内存。

我个人的习惯:在单线程或低并发场景下,ptmalloc完全够用。但一旦线程数超过4个,我就开始考虑换分配器了。

13.2.2 痛点

ptmalloc最大的问题是什么?锁竞争。每个arena内部有锁,多线程频繁分配释放时,线程之间会抢锁。虽然ptmalloc有多个arena来缓解,但arena数量有限(默认是CPU核心数的2倍),线程多了还是抢。

我曾经在一个16核的服务器上跑高并发服务,用ptmalloc时,光是内存分配就占了20%的CPU时间。换成tcmalloc后,直接降到5%以下。

注意:ptmalloc还有一个问题——内存碎片。频繁分配释放小对象时,容易产生外部碎片,导致内存利用率下降。我见过一个服务跑了几天后,RSS涨了30%,但实际有效数据没变多。

13.3 tcmalloc——Google的优化方案

tcmalloc是Google开发的,全称是Thread-Caching Malloc。它的设计思路很直接:每个线程自己缓存一部分内存,减少锁竞争

13.3.1 核心设计

tcmalloc把内存分为三类:

  • 小对象(≤256KB):每个线程有自己的线程本地缓存(Thread Cache),分配释放基本无锁
  • 中对象(256KB~1MB):从中央空闲列表分配,需要加锁
  • 大对象(>1MB):直接用mmap分配

线程缓存满了之后,会批量归还给中央堆。中央堆再统一管理。

关键优势:

  • 小对象分配几乎无锁,多线程性能极好
  • 内存碎片少,因为小对象按固定大小分类(类似slab)
  • 释放速度快,直接放回线程缓存即可

13.3.2 实际体验

我记得有一次优化一个Web服务器,压力测试时发现malloc占了大量CPU。换成tcmalloc后,QPS直接提升了30%。

不过tcmalloc也有缺点:内存占用偏高。因为每个线程都有自己的缓存,线程多了,缓存的总量就上去了。如果你对内存占用很敏感,比如嵌入式环境,tcmalloc可能不太合适。

建议:如果你的应用是多线程、高并发、小对象频繁分配的场景,tcmalloc是首选。我一般在服务端项目里直接链接tcmalloc,省心。

13.4 jemalloc——Facebook的选择

jemalloc最初是FreeBSD的分配器,后来被Facebook大规模使用并优化。它的设计哲学是:减少碎片 + 多核友好

13.4.1 核心设计

jemalloc把内存管理分成多个arena,每个arena独立管理。线程会被分配到某个arena上,减少锁竞争。

jemalloc的内存分类更细:

  • Small class:8字节、16字节、32字节……一直到约14KB
  • Large class:页面大小的倍数,直到约4MB
  • Huge class:超过4MB,直接用mmap

每个大小类都有独立的空闲链表,分配时直接取,释放时直接放回。这样碎片极少。

jemalloc的亮点:

  • 内存碎片控制得非常好,长期运行后内存占用稳定
  • 多核扩展性好,arena数量可配置
  • 有丰富的统计接口,可以查看内存使用详情

13.4.2 实际体验

我在一个需要长时间运行的缓存服务中用过jemalloc。跑了两个月,内存占用几乎没有增长。换成ptmalloc的话,同样的负载,一周后RSS就涨了15%。

jemalloc的缺点呢?配置复杂。它的参数很多,比如arena数量、线程缓存大小、脏页回收策略等。默认配置虽然能用,但想调优得花时间研究。

我曾经踩过的坑:有一次把jemalloc的arena数量设得太少,结果高并发下线程抢arena锁,性能反而比ptmalloc还差。后来改成arena数量等于CPU核心数的4倍,问题解决。

13.5 三者对比

特性 ptmalloc tcmalloc jemalloc
默认使用 Linux glibc 需手动链接 需手动链接
多线程性能 一般(锁竞争) 优秀(线程缓存) 优秀(多arena)
内存碎片 较多 较少 极少
内存占用 较低 较高(线程缓存) 中等
适用场景 单线程/低并发 高并发小对象 长时间运行/大内存
配置复杂度

13.6 如何选择?

说实话,没有银弹。我一般这样选:

  • 简单项目、单线程、嵌入式:ptmalloc就够了,别折腾
  • 高并发Web服务、微服务:tcmalloc,省心省力
  • 长时间运行的缓存、数据库:jemalloc,内存稳定不膨胀
  • 不确定时:三个都跑一下benchmark,看哪个适合你的负载

小技巧:在Linux上,可以用LD_PRELOAD来替换分配器,不用重新编译代码。比如:LD_PRELOAD=/usr/lib/libtcmalloc.so ./myapp

13.7 知识结构图

下面这张图帮你理清三个分配器的核心思路:

三大内存分配器核心设计对比 ptmalloc glibc默认 核心机制: • 多arena管理 • bin分类(fast/small/large) • 主线程用brk • 子线程用mmap 优点: • 系统自带,开箱即用 • 内存占用低 缺点: • 多线程锁竞争 • 内存碎片较多 tcmalloc Google出品 核心机制: • 线程本地缓存 • 小/中/大对象分类 • 中央堆统一管理 • 批量归还机制 优点: • 多线程性能极好 • 小对象分配快 缺点: • 内存占用偏高 • 线程缓存浪费 jemalloc Facebook首选 核心机制: • 多arena + 线程绑定 • 细粒度大小分类 • 脏页延迟回收 • 丰富的统计接口 优点: • 碎片极少 • 长期运行稳定 缺点: • 配置复杂 • 调优需要经验 没有最好的分配器,只有最适合你场景的分配器

13.8 小结

这一章我们聊了三个主流内存分配器。ptmalloc是默认选择,tcmalloc适合高并发,jemalloc适合长时间运行。每个都有自己的设计哲学和适用场景。

嗯,这里要注意:不要盲目追求“最好”的分配器。我见过有人在小项目里硬上jemalloc,结果配置调了半天,性能还不如ptmalloc。先搞清楚你的瓶颈在哪,再选工具。

下一章我们会深入ptmalloc的源码实现,看看它到底是怎么管理内存的。到时候你会发现,原来malloc背后藏着这么多门道。


公众号:蓝海资料掘金营,微信deep3321