13、内存分配器原理:ptmalloc、tcmalloc、jemalloc简介
说到C语言的内存管理,很多人第一反应就是malloc和free。但你想过没有,这两个函数背后到底是谁在干活?
说白了,真正管内存的,是内存分配器。我这些年做嵌入式和高并发服务,跟这三个分配器打过不少交道——ptmalloc、tcmalloc、jemalloc。每个都有自己的脾气。
13.1 为什么需要内存分配器?
操作系统提供了brk和mmap来分配内存,但这两个是系统调用,每次调用都要陷入内核。你想想看,如果每次malloc都触发一次系统调用,那性能得多差?
所以内存分配器的作用就是:提前从系统申请一大块内存,然后自己管理,按需分给应用程序。这样大部分malloc和free都在用户态完成,速度飞快。
核心目标:
- 减少系统调用次数
- 减少内存碎片
- 提高分配和释放的速度
- 多线程环境下尽量不互相干扰
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 知识结构图
下面这张图帮你理清三个分配器的核心思路:
13.8 小结
这一章我们聊了三个主流内存分配器。ptmalloc是默认选择,tcmalloc适合高并发,jemalloc适合长时间运行。每个都有自己的设计哲学和适用场景。
嗯,这里要注意:不要盲目追求“最好”的分配器。我见过有人在小项目里硬上jemalloc,结果配置调了半天,性能还不如ptmalloc。先搞清楚你的瓶颈在哪,再选工具。
下一章我们会深入ptmalloc的源码实现,看看它到底是怎么管理内存的。到时候你会发现,原来malloc背后藏着这么多门道。