malloc
在底层区分小内存和大内存分配,主要是为了在性能 、内存利用率 和碎片控制这几个关键目标上取得最佳平衡。
这种区分是多年实践和优化的结果,其主要原因可以归纳为以下几点:
1. 应对不同分配模式的需求
程序的内存分配请求通常呈现出一种"二八定律"或类似模式:
- 频繁分配和释放的都是小块内存 (如对象、结构体、字符串等)。对这些操作,速度是首要目标。
- 偶尔才会分配大块内存 (如大的缓冲区、数组、图像数据等)。对这些操作,避免内存浪费和方便管理更为重要。
为这两种截然不同的模式采用同一种分配策略是低效的。因此,malloc
采用了分级分配的策略。
2. 减少内存碎片(Fragmentation)
内存碎片是内存分配器的天敌,分为内部碎片和外部碎片。
-
内部碎片:分配器分配给程序的内存块比程序实际请求的大,这中间浪费的空间就是内部碎片。
-
外部碎片:空闲内存被分散成许多不连续的小块,导致无法满足较大的分配请求,即使总空闲空间足够。
-
对于小内存 :分配器(如 glibc 的 ptmalloc)会预先通过
brk/sbrk
申请一大块内存(称为 Heap 或 Arena ),并将其划分为各种固定大小的内存池 (也称为 bins,例如 16字节、24字节、32字节...的桶)。- 当程序申请小块内存时,分配器会快速找到一个合适大小的桶,从中分配一块。
- 释放时,内存块回到对应的桶中。
- 好处 :由于大小固定,分配和释放速度极快。并且能有效减少外部碎片,因为只有相同大小的内存块会在池子中交换。
-
对于大内存 :使用
mmap
单独映射一块内存。- 这块内存的大小正好等于请求的大小(加上一些元数据开销)。
- 好处 :分配和释放都非常直接。释放时通过
munmap
将整块内存直接归还给操作系统,完全避免了这块内存区域的碎片问题(无论是内部还是外部碎片)。
3. 性能优化
- 小内存分配 :在预先分配的内存池中操作,主要是在用户态进行链表操作,速度非常快 。并且由于操作的是连续的内存区域,缓存局部性(Cache Locality)更好。
- 大内存分配 :虽然
mmap
系统调用有一定开销,但大内存分配本身就不频繁,这种开销是可接受的。更重要的是,munmap
释放大内存后,能立刻将物理页归还给操作系统,减轻系统整体内存压力。
4. 降低系统调用开销
系统调用(如 brk
和 mmap
)需要从用户态切换到内核态,开销很大。
- 通过
brk
一次性扩展堆空间来服务大量的小内存请求 ,可以摊薄(Amortize) 每次分配的系统调用开销。成百上千次小分配可能只源于一两次brk
调用。 - 对于大内存 ,由于其不频繁,即使每次分配都调用一次
mmap
,总体开销也是可控的。
5. 便于内存归还操作系统
这是一个关键但常被忽略的点。
brk
分配的堆内存 :释放小内存块时,它只是回到分配器的空闲列表中,并不会立即触发收缩堆的操作(brk
系统调用减少program break)。因为分配器预期程序很快就会再次申请同样大小的内存。收缩堆是一个昂贵的操作,而且可能只在堆顶部的内存全部空闲时才能进行 。因此,通过brk
分配的内存很难彻底还给操作系统。mmap
分配的大内存 :由于是独立映射的,free
这类内存时,可以立即调用munmap
,干净利落地将整块内存连同理物理页一起归还给操作系统。这对于长时间运行的程序(如守护进程、服务端程序)非常重要,可以防止它们的内存占用(RSS)无限增长。
总结对比
特性 | 小内存分配 (通过 brk ) |
大内存分配 (通过 mmap ) |
---|---|---|
设计目标 | 速度 、低碎片化 | 易于管理 、避免碎片 、易于归还 |
分配策略 | 从预先分配的大小分类的内存池(bins) 中获取 | 直接从操作系统独立映射一块所需大小的内存 |
碎片 | 主要产生内部碎片,有效控制外部碎片 | 几乎无碎片(分配和释放都是整块进行) |
系统调用 | 开销被摊薄 (多次分配对应一次brk ) |
每次分配/释放都对应一次 mmap /munmap |
归还系统 | 困难且延迟,只在堆顶空闲时才能收缩 | 立即且彻底 ,free 即调用 munmap |
适用场景 | 频繁、大量的小块内存请求 | 不频繁的大块内存请求 |
这种"大小分离"的设计是现代内存分配器(如 glibc 的 ptmalloc)高效工作的基石。它聪明地针对不同规模的内存请求采用了最合适的策略,从而在整体上提供了优异的性能。