最大的误会:以为 malloc 是操作系统给的函数,上层只是转发调用。真相恰恰相反——本页用图和可交互演示讲清:内核只给粗粒度的 brk/mmap,malloc 你自己进程的库函数,而它(以及更上层的专用分配器)做的事远比你想象的多。
点下方每一层,看它到底归谁、干什么。重点看第二层——malloc / free 是跑在你进程里的用户态库函数,不在内核里。
所以你问的「上层自己还能做啥」——那个“上层”(libc 里的 malloc)本身就是分配器,它才是干绝大多数活的角色。内核只负责“批发整页的虚拟地址”,完全不知道你那个 17 字节的对象长什么样、什么时候该释放。
更往上,应用还能在 libc malloc 之上再写专用分配器(对象池、临时区分配器、GC 堆等)。下面几节逐一拆解。
内核提供给用户程序的,只有两类非常粗粒度、且昂贵的原语:
那如果“每次分配都直接问 OS”会怎样?点下面按钮看看 1000 次 20 字节分配的下场👇
注:真实 malloc 还有 chunk 头(几十字节)等元数据开销,这里为对比做了简化,但“naive 浪费 ~99% 内存 + 1000 次系统调用”的结论是成立的。
拿到 OS 批发来的几页虚拟内存后,分配器要在用户态扮演一个“迷你操作系统”,把大块切成任意大小的碎片并高效管理。它的核心职责:
下面是一段堆。先分配 A/B/C,再释放中间的 B 形成空洞,然后分配一个稍大的 D——看普通分配器会“塞不进空洞”而浪费空间;尺寸分级怎么解决这个问题。
你申请的空间会被向上取整到某个“尺寸等级”,多出来的就是内部碎片。输入想要的字节数,看它实际分到哪一级。
既然都要“切分+记账+回收”,为什么还有 ptmalloc、jemalloc、tcmalloc、mimalloc… 以及应用层的对象池、区域分配器?因为它们针对的瓶颈不同。点卡片看各自解决什么问题。
glibc 自带。单堆 + 多线程 arena + bin。够用但多核下全局锁易成瓶颈。
Facebook 出品。arena + size class + 着色,碎片的长期占用极低,多核扩展好。
Google 出品。每线程缓存(mcache)让小对象分配基本无锁,Go 分配器受其启发。
微软出品。用“自由表”设计,元数据极轻,单线程/多线程都很快,代码量小。
Arena / bump allocator:只进不退,最后整体一次性释放。解析器、编译器、帧内存超快。
固定大小对象反复创建销毁(游戏实体、网络连接)。零碎片、零搜索,O(1) 取放。
Go/Java/V8:分配器与垃圾回收协作,程序员不写 free,由运行时判断对象死活后回收。
Linux 内核用:为内核对象(如 task_struct)做定长缓存,避免内核自己频繁切分。
| 角色 | 在哪 | 给你的原语/接口 | 粒度 | 要不要进内核 |
|---|---|---|---|---|
| 物理内存 | 硬件 | 电容/页框 | 字节(硬件) | — |
| 操作系统内核 | 内核态 | brk/sbrk/mmap/munmap | 整页(4KB) | 是(系统调用) |
| C 运行库 malloc | 用户态(libc) | malloc/free/calloc | 任意字节 | 否(偶尔补货) |
| 应用专用分配器 | 用户态(你的代码) | 自定义 API | 任意/定长 | 否 |