malloc 的底层实现依赖于 C 库,通常使用 ptmalloc(glibc 默认分配器)、tcmalloc(Google)或 jemalloc(FreeBSD 等)等。
下文详细阐述 glibc 的 ptmalloc 机制下的 malloc 的底层实现
基本概念
内存分配器:管理堆内存,处理内存的分配和释放。
块(Chunk):内存分配的基本单位,每个分配的内存块都有一个头部的元数据。
空闲链表(Free List):用于管理空闲块,常见的有单向链表和双向链表。
箱(Bins):用于存放不同大小的空闲块,提高分配效率。
内存布局
在 glibc 中,malloc 管理的内存通常通过 brk 或 mmap 系统调用从操作系统获取。
brk:扩展堆的顶部,用于较小内存分配。
mmap:创建匿名内存映射,用于大内存分配(通常超过 MMAP_THRESHOLD,默认为 128KB)。
块(Chunk)结构
每个内存块(包括已分配和空闲的)都有一个头部的元数据,定义如下:
struct malloc_chunk {
size_t prev_size; /* 前一个块的大小(如果前一个块是空闲的) */
size_t size; /* 当前块的大小和状态标志 */
struct malloc_chunk* fd; /* 双向链表的前驱指针(仅空闲块有效) */
struct malloc_chunk* bk; /* 双向链表的后继指针(仅空闲块有效) */
};prev_size:如果前一个块是空闲的,这个字段存储前一个块的大小;如果前一个块是已分配的,则这个字段可以被前一个块使用(作为用户数据的一部分)。
size:当前块的大小,由于内存对齐,大小的低3位用作标志位:
PREV_INUSE(最低位):前一个块是否已分配。IS_MMAPPED(次低位):当前块是否通过mmap分配。NON_MAIN_ARENA(第三位):是否不属于主分配区。
空闲块管理
空闲块通过双向链表连接,这些链表称为“bins”。ptmalloc 有多种类型的 bins:
Fast bins:单向链表,管理小内存块(默认小于 64B),后进先出,不合并空闲块。
Small bins:双向链表,管理小于 512B 的块,每个 bin 中的块大小相同,先进先出。
Large bins:双向链表,管理大于等于 512B 的块,每个 bin 中的块大小在一个范围内,按大小排序。
Unsorted bin:双向链表,存放刚刚释放的块,用于快速重新分配。
分配流程
小内存分配(< 64B):
首先在 fast bins 中查找对应大小的空闲块。
如果找到,则取出并返回。
中等内存分配(64B ~ 512B):
在 small bins 中查找。
如果 small bin 为空,则从 unsorted bin 中查找并分割。
大内存分配(>= 512B):
在 large bins 中查找最合适的块。
如果找不到,则从更大的 large bin 中分割,或者从 top chunk 分割。
如果上述步骤都失败:
扩展 top chunk(通过
brk或mmap)并从中分配。如果需要分配的内存很大(超过
MMAP_THRESHOLD),则直接使用mmap分配。
释放流程
检查前后块是否空闲,如果空闲则合并,避免碎片。
放入 unsorted bin,以便下次分配时快速重用。
如果块大小适合 fast bin,则放入 fast bin(不合并)。
1. malloc 的基本流程
当调用 malloc(size) 时,底层会执行以下操作:
检查请求大小:
若
size == 0,可能返回NULL或一个特殊指针(依赖实现)。若
size过大(如超过MMAP_THRESHOLD,默认通常为 128KB),直接使用mmap分配。
从空闲内存链表中查找:
在堆内存的空闲块(
free list)中搜索足够大的内存块。使用算法(如
ptmalloc的隐式链表或显式链表)匹配最佳块。
分割或分配新内存:
若找到的块比需求大,可能分割剩余部分放回空闲链表。
若没有足够空间,调用
sbrk或mmap向操作系统申请更多内存。
返回内存地址:
返回分配的内存地址(通常位于堆区),用户可通过指针访问。
2. 底层内存分配方式
malloc 最终依赖两种系统调用分配内存:
(1) brk/sbrk(小内存分配)
原理:通过移动堆顶指针(
program break)扩展堆内存。特点:
适用于小内存请求(如
< 128KB)。分配速度快,但频繁调用可能导致内存碎片。
示例:
c复制代码
void *malloc(size_t size) { if (size <= MMAP_THRESHOLD) { return sbrk(size); // 扩展堆空间 } // ... }
(2) mmap(大内存分配)
原理:直接映射匿名内存页到进程地址空间(不依赖堆)。
特点:
适用于大内存请求(如
>= 128KB)。避免堆碎片,但系统调用开销较大。
示例:
c复制代码
void *malloc(size_t size) { if (size >= MMAP_THRESHOLD) { return mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); } // ... }
3. 内存管理算法
不同 malloc 实现(如 glibc 的 ptmalloc、jemalloc、tcmalloc)使用不同算法优化性能:
(1) ptmalloc(glibc 默认)
空闲链表(Bins):
将空闲块按大小分类(如
fast bins、small bins、large bins)。快速匹配小内存请求,减少搜索时间。
线程本地缓存(
tcache):每个线程维护独立的内存池,避免全局锁竞争。
(2) jemalloc(Facebook/FreeBSD)
多级内存池:
按线程和大小分层管理,减少碎片和锁冲突。
适用于高并发场景。
(3) tcmalloc(Google)
线程缓存 + 全局缓存:
小对象优先从线程缓存分配,大对象走全局堆。
减少锁争用,适合多线程应用。
4. 内存碎片问题
(1) 内部碎片
原因:分配的内存块比实际需求大(如对齐填充)。
示例:请求 10B,但分配 16B(因对齐要求)。
(2) 外部碎片
原因:频繁分配/释放导致内存块分散,无法合并成大块。
解决方案:
合并相邻空闲块(
coalescing)。使用
mmap分配大块内存避免堆碎片。
5. free 的工作原理
调用 free(ptr) 时:
检查指针有效性:若
ptr == NULL,直接返回。判断内存来源:
若来自
mmap,直接调用munmap释放。若来自堆,将内存块标记为空闲并放回链表(可能合并相邻块)。
延迟归还操作系统:
空闲内存可能保留在进程堆中,供后续
malloc复用,而非立即归还系统(通过M_TRIM_THRESHOLD控制)。
6. 性能优化建议
避免频繁小内存分配:
预分配大块内存,自行管理(如对象池)。
选择合适分配器:
多线程应用优先用
jemalloc或tcmalloc。
监控内存使用:
工具:
valgrind、malloc_stats()、pmap。