在 Linux 系统中,堆管理是一个分层架构:底层由内核(Kernel)提供原始空间,上层由glibc 库的ptmalloc2分配器负责精细化的块管理。
1. 宏观层级:Arena(分配区)
为了支持多线程,堆被划分为多个arena结构。ptmalloc2 根据分配方式和位置将 Arena 分为两类:
- 主分配区 (Main Arena):整个进程只有一个, 其malloc_state结构是全局变量,存储在libc.so的数据段中,主要通过brk系统调用扩展堆空间,虽然也可以使用mmap;
- 非主分配区 (Non-main Arena / Thread Arena):为减少多线程锁竞争而动态创建,其malloc_state直接存储在它所管理的第一个堆分段(Heap)的起始位置,通过mmap分配;
主分配区和非主分配区的区别:
| 特性 | 主分配区(Main Arena) | 非主分配区(Thread Arena) |
| 存储位置 | malloc_state结构是全局变量,位于libc.so的数据段 | 位于mmap申请的第一个堆段的起始处 |
| 内存来源 | 通过sbrk扩展(brk区域)或mmap申请 | 完全通过mmap申请 |
| 关联结构 | 不含heap_info | 第一个堆段包含heap_info和malloc_state |
| 数量 | 整个进程只有一个 | 为减少多线程锁竞争而动态创建 |
1.1 主分配区结构malloc_state
ptmalloc2通过malloc_state结构来管理内存的分配。每个 Arena的元数据都存储在一个名为malloc_state的状态机结构体中(在源码中通常被称为mstate)。它记录了当前分配区内所有空闲内存块(bins)、分配策略和内存边界信息。它包含以下关键字段:
struct malloc_state
{
/* 保护该分配区的互斥锁 */
__libc_lock_define (, mutex); //线程锁,当多线程进行内存分配竞争的时候,需要首先拿到该锁才能进行分配区上的操作
/* 标志位:记录分配区是否有连续内存、是否包含 fastbins 等 */
int flags; //记录了分配区的一些标志
/* 是否设置了单线程模式的优化 */
int have_fastchunks; //用于标记是否有fast_bins
/* fastbins数组:存放LIFO的小块内存(通常 16-80 字节) */
mfastbinptr fastbinsY[NFASTBINS]; //fastbins是bins的高速缓冲区,大约有10个定长队列
/* 指向当前堆顶部的top chunk*/
mchunkptr top; //top chunk相当于分配区的顶部空闲内存,当bins都不能满足分配要求的时候,就会来top chunk分配
/* 最近释放但尚未放入 Small/Large Bins 的 Chunk(Last Remainder Chunk) */
mchunkptr last_remainder; //最新的chunk分割之后剩下的那部分
/* 常规 Bins 数组:包含 Unsorted Bin(1), Small Bins(62), Large Bins(63) */
/* 数组下标 1 为 Unsorted Bin;后续为 Small/Large Bins */
mchunkptr bins[NBINS * 2 - 2]; //用于存储unsorted bin、small bins和large bins的chunk链表
/* 快速查找位图:每一位代表对应的bin是否有空闲块 */
unsigned int binmap[BINMAPSIZE]; //用一个bit来标识代表对应的bin是否有空闲chunk
/* 链表指针:指向下一个分配区(Arena) */
struct malloc_state *next; //分配区全局链表,主分配区放头部,新加入的分配区放main_arean.next位置
/* 备用分配区链表(用于线程冲突时快速切换) */
struct malloc_state *next_free; //空闲的分配区
INTERNAL_SIZE_T attached_threads; //
/* 该分配区当前占用的系统内存总量 */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
1.2 非主分配区结构heap_info
glibc malloc(ptmalloc)的内存管理机制中,heap_info结构体主要用于管理 非主分配区(Non-main Arena) 的堆内存。
非主分配区的内存可以由多个不连续的mmap区域组成,这些区域被称为Heap。每个Heap的开头都有一个heap_info结构。heap_info结构记录了当前Heap的大小、已分配空间以及指向所属malloc_state的指针。多个Heap通过prev指针链接起来,构成一个非主分配区的空间需求。
heap_info通常位于由mmap分配的每个新堆段(Heap Segment)的起始位置,源码如下:
typedef struct _heap_info {
mstate ar_ptr; /* 指向该堆所属的分配区(Arena),即arena的地址 */
struct _heap_info *prev; /* 指向前一个堆的 heap_info,构成单向链表 */
size_t size; /* 当前堆的大小(以字节为单位) */
size_t mprotect_size; /* 已受保护(通常是已映射)的大小 */
/* 最后可能包含填充字节以确保内存对齐 */
char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK]; /*0x10字节对齐,X86中是8字节对齐*/
} heap_info;
heap_info结构的特性:
- 仅限非主分配区:主分配区(Main Arena)使用系统的brk区域,空间是连续的,因此不需要heap_info来管理多个离散的堆段;
- 内存布局:在非主分配区中,第一个堆的heap_info结构之后紧跟着分配区头部(malloc_state),而后续增加的堆则只包含heap_info;
- 地址对齐:每个由mmap分配的堆都会对齐到HEAP_MAX_SIZE边界。这使得malloc可以通过简单的位运算(掩码)快速定位到当前指针所属的heap_info地址。
2. 微观层级:Chunk(内存块)
堆空间的最小单位是chunk。无论你申请多大的内存,ptmalloc2都会将其封装为这种结构。
ptmalloc2设计精妙之处在于内存复用。当一个chunk被使用时,其元数据(fd/bk)区域会被用户数据覆盖;当它被释放时,这些区域又重新变回链表指针。这种设计最小化了内存分配的额外开销(Overhead)
2.1 结构体
Chunk结构体malloc_chunk定义在 Glibc 源码的 malloc.c 中,其内存布局随状态(分配或空闲)而变:
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /*如果前一个chunk是空闲的,则记录其大小;如果前一个chunk已经被使用,该字段空间可以被前一个chunk的用户数据空间复用*/
INTERNAL_SIZE_T size; /* 当前chunk的大小,低3位用于标志位 */
/*当chunk空闲的时候,放置到bin上双向链表管理,fd指向下一个(非物理相邻)空闲的chunk,bk指向下一个(非物理相邻)空闲的chunk,只有当chunk空闲的时候,才会放到bin上进行空闲管理,占用的是用户数据区域*/
struct malloc_chunk* fd; /* 双向链表:指向后一个空闲chunk*/
struct malloc_chunk* bk; /* 双向链表:指向前一个空闲chunk*/
/* 仅用于管理Large Bin中的空闲chunk双向链表管理,一般空闲的Large chunk在fd的遍历顺序中从大到小排列,也是复用用户数据区域*/
struct malloc_chunk* fd_nextsize; /* 指向下一个不同大小的空闲块 */
struct malloc_chunk* bk_nextsize; /* 指向前一个不同大小的空闲块 */
};
2.2 chunk的生存状态
2.2.1 chunk使用时(已分配)的状态
当前的chunk如果正在被使用中,即已分配状态,它的物理相邻的下一个chunk的prev_size字段是无效的,所以这个字段就可以被当前这个chunk使用。
复用相邻下一个nextchunk的mchunk_prev_size字段的数据空间,由于chunk被使用中,所以不需要通过双向链表方式挂载到空闲bins上管理。
2.2.2 chunk空闲时的状态
当chunk为空闲状态时,chunk会被挂载到bins空闲链表上进行管理。fd和bk指针生效,fd/bk以及fd_nextsize/bk_nextsize的指针地址复用了用户数据区域的空间。
2.3 相关宏
ptmalloc2的malloc.c文件中,由于malloc_chunk结构体需要兼顾已分配和空闲状态,且为了内存对齐和性能,大量操作是通过宏 (Macros) 直接处理位运算来实现的。
由于C语言结构体无法直接表达“物理重叠”和“位标志”,这些宏承担了指针计算和状态转换的重任。
2.3.1 字段偏移与指针转换
这组宏用于在用户指针(mem)和chunk指针(p)之间转换,mem指向用户得到的内存的起始位置:
/* conversion from malloc headers to user pointers, and back */
#define chunk2mem(p) ((void *) ((char *) (p) + 2 * SIZE_SZ)) //将指向chunk起始地址的指针转换为指向用户可用数据的mem地址。chunk起始地址在低地址,再加上2*SIZE_SZ的方式转换到高地址的mem地址指针,即 (char*)(p) + 2*SIZE_SZ;
#define mem2chunk(mem) ((mchunkptr)((char *) (mem) -2 * SIZE_SZ)) //逆向操作,将指向用户可用数据的mem地址转换为指向chunk起始地址的指针,用户内存mem地址在高地址,再减去2*SIZE_SZ的方式转换到低地址的chunk的起始地址;
/* The smallest possible chunk */
#define MIN_CHUNK_SIZE (offsetof(struct malloc_chunk, fd_nextsize)) //堆管理器的最小物理chunk大小,这个宏的存在是为了确保任何chunk在空闲(free)时,其空间至少能容纳下最基本的双向链表指针;由于指针宽度不同,MINSIZE的值在不同架构下也是不同的:64位系统chunk最小为32字节,32位系统chunk最小为16字节
2.3.2 规格与对齐宏
对齐宏用于将用户请求的任意大小(request size)转换为符合系统要求的chunk规格:
/* The smallest size we can malloc is an aligned minimal chunk */
//MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1
//最小chunk大小,64位系统为32,32位系统为16
//用户最小申请的内存大小必须是2 * SIZE_SZ的最小整数倍。
//区别于MIN_CHUNK_SIZE,它是MIN_CHUNK_SIZE经过内存对齐(Alignment)处理后的最终对齐维度
#define MINSIZE \
(unsigned long) (((MIN_CHUNK_SIZE + MALLOC_ALIGN_MASK) & \
~MALLOC_ALIGN_MASK))
- MIN_CHUNK_SIZE是底线:它确保了当一个chunk被 free 掉放入unsorted bin或small bin时,绝对不会因为空间太小而覆盖不了fd/bk指针;
MINSIZE是标尺:在request2size宏计算用户请求时,代码会通过类似if (sz < MINSIZE) sz = MINSIZE;的逻辑,确保任何微小的分配请求(如malloc(1)最终拿到的内存块都至少是MINSIZE这么大;
也就是说,MIN_CHUNK_SIZE是为了“装得下”管理数据,而MINSIZE是为了“对得齐”内存边界。
/* Check if m has acceptable alignment */
// MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1
//aligned_OK(m) 是一个用于验证数值或地址是否符合内存对齐要求的宏。
#define aligned_OK(m) (((unsigned long) (m) & MALLOC_ALIGN_MASK) == 0)
// misaligned_chunk是一个用于检测内存块(chunk)地址是否对齐的宏,即检查给定的 malloc_chunk 指针p是否符合系统要求的内存对齐边界(通常是8或16字节对齐)。如果chunk未对齐,该宏会返回一个非零值(通常是未对齐的字节偏移量),否则返回0。
#define misaligned_chunk(p) \
((uintptr_t)(MALLOC_ALIGNMENT == 2 * SIZE_SZ ? (p) : chunk2mem(p)) & \
MALLOC_ALIGN_MASK)
/*
Check if a request is so large that it would wrap around zero when
padded and aligned. To simplify some other code, the bound is made
low enough so that adding MINSIZE will also not wrap around zero.
*/
//REQUEST_OUT_OF_RANGE(s) 是一个用于验证申请的内存大小是否合法(是否越界)的预处理宏。
//该宏的主要作用是检查用户请求分配的字节数(或转换后的 chunk 大小)是否超出了系统能够处理的最大范围
#define REQUEST_OUT_OF_RANGE(req) \
((unsigned long) (req) >= (unsigned long) (INTERNAL_SIZE_T)(-2 * MINSIZE))
/* pad request bytes into a usable size -- internal version */
//MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1
//request2size将用户请求的字节数(req)转换为符合系统要求的内部chunk大小(sz)。
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) \
? MINSIZE \
: ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
/* Same, except also perform argument check */
//checked_request2size(req, sz) 是一个关键的安全宏,它负责将用户请求的内存字节数(req)转换为符合对齐要求的内部chunk大小(sz),同时进行溢出检查。
#define checked_request2size(req, sz) \
if (REQUEST_OUT_OF_RANGE(req)) { \
__set_errno(ENOMEM); \
return 0; \
} \
(sz) = request2size(req);
2.3.3 状态位相关宏
这些宏通过掩码操作size字段的低3位:
/* size field is or'ed with PREV_INUSE when previous adjacent chunk in use */
#define PREV_INUSE 0x1 //PREV_INUSE(全大写)通常指代那个特定的位掩码常量,而prev_inuse(p)(小写)则是使用这个掩码的宏函数
/* extract inuse bit of previous chunk */
#define prev_inuse(p) ((p)->mchunk_size & PREV_INUSE) //prev_inuse是一个用于状态检测的位运算宏,它负责读取chunk的size字段中的最低位,作用是判断物理相邻的前一个chunk是否处于“使用中”状态
/* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */
#define IS_MMAPPED 0x2 //IS_MMAPPED 是 size 字段中的第二个状态标志位(掩码常量),它用于标识当前内存块的分配来源
//位值为1表示该块是通过mmap系统调用直接从操作系统分配的
//位值为0表示该块是从常规的Arena(堆区/主分配区) 中通过sbrk或扩展已有的堆段分配的
/* check for mmap()'ed chunk */
#define chunk_is_mmapped(p) ((p)->mchunk_size & IS_MMAPPED) //chunk_is_mmapped(p) 是一个用于状态检查的宏函数,专门用来读取 IS_MMAPPED (0x2) 标志位,用于确定这个堆块是由mmap独立申请的,还是在Arena(常规堆区)内分配的,
//返回非零(true)就是mmap分配,不属于任何Arena,也没有fd/bk指针
//返回零(false)就属于常规堆,由brk或sbrk管理,释放后会进入Bins
/* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained
from a non-main arena. This is only set immediately before handing
the chunk to the user, if necessary. */
#define NON_MAIN_ARENA 0x4 //NON_MAIN_ARENA是size字段中三个状态标志位里的最后一位(掩码常量),用于区分内存块的分配区位置
//位值为0:该chunk属于主分配区(Main Arena)。主分配区管理通过brk/sbrk扩展的传统堆段
//位值为1 (0x4):该chunk属于非主分配区(Thread Arena)。这些Arena是为多线程环境创建的,通常位于通过mmap分配的Heap Subsurface(分段堆) 中。
/* Check for chunk from main arena. */
#define chunk_main_arena(p) (((p)->mchunk_size & NON_MAIN_ARENA) == 0) //chunk_main_arena(p) 是一个用于判断堆块“归属地”的宏函数。堆管理器使用此宏来区分两种存储区域
//它通过检查size字段的NON_MAIN_ARENA(0x4)标志位来判断如果该位为0,宏返回真(True),表示该chunk属于主分配区(Main Arena)
/* Mark a chunk as not being on the main arena. */
#define set_non_main_arena(p) ((p)->mchunk_size |= NON_MAIN_ARENA) //set_non_main_arena(p) 是一个用于状态标记的位运算宏,作用是标记该chunk属于非主分配区(Thread Arena)
/*
Bits to mask off when extracting size
Note: IS_MMAPPED is intentionally not masked off from size field in
macros for which mmapped chunks should never be seen. This should
cause helpful core dumps to occur if it is tried by accident by
people extending or adapting this malloc.
*/
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) //SIZE_BITS是一个非常关键的位掩码常量,它定义了chunk的size字段中用于存储“状态标志”的位数
//由于这三个标志位分别是0x1,0x2,0x4,因此SIZE_BITS的数值通常为0x7
//这个宏的主要作用是作为过滤器,将“块的大小数值”与“块的状态标志”隔离开来
/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask(p) & ~(SIZE_BITS)) //用于获取一个chunk的实际物理大小(字节数)
//由于ptmalloc为了节省空间,将PREV_INUSE等标志位“藏”在了size字段的低三位中,直接读取mchunk_size会得到一个比实际稍大的奇数
/* Like chunksize, but do not mask SIZE_BITS. */
#define chunksize_nomask(p) ((p)->mchunk_size) //它与chunksize的功能相同,但省略了位掩码操作
//在传统的chunksize(p)中,每次获取大小都需要进行一次& ~0x7的位运算
/* extract p's inuse bit */
#define inuse(p) \
((((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size) & PREV_INUSE)
//inuse宏用于判断一个 chunk 指针 p 当前是否正被用户使用(即处于 Allocated 状态,而非在 Bin 链表中)
/* set/clear chunk as being inuse without otherwise disturbing */
#define set_inuse(p) \
((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size |= PREV_INUSE
//set_inuse(p) 是一个用于锁定当前chunk状态的组合宏。它通过操作“邻居”块的标志位,正式宣告chunk p 进入“在使用中(Allocated)”状态,标记为正在被使用
//这个宏实现了ptmalloc的核心设计逻辑:一个chunk的状态记录在它物理相邻的下一个块中。
//定位到next_chunk(p)的起始地址,将该邻居块size字段的PREV_INUSE (0x1)位设为1。
//当堆管理器扫描到next_chunk时,看到P位为1,就知道前方的p正在被用户使用,严禁将其合并
#define clear_inuse(p) \
((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size &= ~(PREV_INUSE)
//clear_inuse(p)是一个用于释放当前chunk状态的组合宏。它的逻辑与set_inuse完全相反,用于宣告chunk p进入“空闲(Free)”状态
//在ptmalloc的设计中,一个chunk p是否空闲,状态标记在它的物理后块(next_chunk)的size字段(P 位)里
//通过chunksize(p)定位到物理相邻的下一个chunk
//将该邻居块的mchunk_size字段中的PREV_INUSE(0x1) 位清零。
//告诉堆管理器,位于地址p的块现在可以被合并或重新分配了。
2.3.4 邻接chunk定位和操作宏
该组宏利用size字段在内存中前后“跳转”:
/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr)(((char *) (p)) + chunksize(p))) //next_chunk(p)是堆管理器在内存空间中“向前导航”的核心宏。它利用当前chunk的大小,精确计算出在物理内存中紧随其后的下一个chunk的起始地址。
/* 将当前chunk指针p减去其prev_size字段的值,得到前一个chunk的指针 */
#define prev_chunk(p) ((mchunkptr)((char*)(p) - ((p)->prev_size)))
//prev_chunk(p)是堆管理器在内存空间中“向后导航”的核心宏。它利用当前chunk的大小,精确计算出在物理内存中的上一个chunk的起始地址。
/* Size of the chunk below P. Only valid if !prev_inuse (P). */
#define prev_size(p) ((p)->mchunk_prev_size) //获取chunk p的prev_size字段值,只有当前一个物理相邻的chunk处于 空闲(Free) 状态时,当前chunk的prev_size字段才有效
/* Set the size of the chunk below P. Only valid if !prev_inuse (P). */
#define set_prev_size(p, sz) ((p)->mchunk_prev_size = (sz)) //将chunk p的prev_size字段设置为 s。定义通常为 ((p)->prev_size = (s))。
/* Ptr to previous physical malloc_chunk. Only valid if !prev_inuse (P). */
#define prev_chunk(p) ((mchunkptr)(((char *) (p)) - prev_size(p))) //这是最关键的衍生宏。通过当前chunk指针p减去prev_size,计算并返回物理相邻的前一个chunk的起始地址。
2.3.5 设置chunk的size字段宏(状态位操作)
该组宏用于在分配或释放时更新 chunk 状态:
/* Set size at head, without disturbing its use bit */
// SIZE_BITS = 7
#define set_head_size(p, s) \
((p)->mchunk_size = (((p)->mchunk_size & SIZE_BITS) | (s))) //set_head_size是一个内部使用的宏,用于设置一个malloc_chunk结构体的size字段,同时保留该字段原有的标志位
/* Set size/use field */
#define set_head(p, s) ((p)->mchunk_size = (s)) //区别于set_head_size,会直接覆盖。它不关心原来的标志位,直接把size设为s。通常用于新分配内存或初始化一个伪造的chunkk
/* Set size at footer (only when chunk is not in use) */
#define set_foot(p, s) \
(((mchunkptr)((char *) (p) + (s)))->mchunk_prev_size = (s))
//set_foot(p, s)是一个与set_head配对使用的同步宏。它的核心作用是:在当前chunk的末尾(即下一个物理相邻chunk的起始位置)写入当前块的大小(prev_size)
2.3.6 获取指定偏移处的chunk相关宏
该组宏用于指定偏移处chunk使用状态相关操作:
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr)(((char *) (p)) + (s))) //它用于快速定位距离当前chunk指针p偏移量为s字节处的另一个chunk,可以跳过任意字节数s
/* check/set/clear inuse bits in known places */
#define inuse_bit_at_offset(p, s) \
(((mchunkptr)(((char *) (p)) + (s)))->mchunk_size & PREV_INUSE)
//inuse_bit_at_offset(p, s) 是一个用于远程探测标志位的关键宏。它允许分配器跳过当前chunk p,直接检查距离偏移量为s处的那个chunk的PREV_INUSE位。
#define set_inuse_bit_at_offset(p, s) \
(((mchunkptr)(((char *) (p)) + (s)))->mchunk_size |= PREV_INUSE)
//set_inuse_bit_at_offset(p, s) 是一个用于状态设置的位运算宏。它允许分配器跳过当前chunk p,直接将距离偏移量为s处的那个chunk的PREV_INUSE位设置为1
#define clear_inuse_bit_at_offset(p, s) \
(((mchunkptr)(((char *) (p)) + (s)))->mchunk_size &= ~(PREV_INUSE))
//clear_inuse_bit_at_offset(p, s) 是一个用于状态撤销的位运算宏。它的作用是:将距离当前chunk p 偏移量为s处的那个chunk的PREV_INUSE位清零
3. 中观层级:Bin(空闲链表)
Linux系统中,ptmalloc采用分箱式方法(Binning)对空闲的chunk进行管理。简单来说,就是将大小相似的空闲块(chunks)归类到不同的箱子(Bins)中。Bins机制本质上是一组空闲内存块(Free Chunks)的链表容器。
释放掉的chunk不会立即归还给内核,而是由ptmalloc分配器根据chunk大小存放在不同的bins链表中以便复用。当用户再一次请求分配内存时,ptmalloc分配器会尝试在空闲的chunk中挑选一块合适的给用户。这样可以避免频繁的系统调用,降低内存分配的开销。
Bin是用于管理已释放内存块(chunk)的链表结构。为了提高分配效率并减少碎片,ptmalloc根据chunk的大小和状态将其分类存放在不同的Bins中。
ptmalloc主要维护以下四类Bin,它们存储在malloc_state结构体的fastbinsY数组和bins数组中:
| Bins类型 | 数量 | 管理方式 | 特点 |
| Fast Bin | 10 | 单链表 (LIFO) | 存放绝大多数小尺寸chunk,不合并相邻空闲块,分配最快 |
| Unsorted Bin | 1 | 双向链表 (FIFO) | 释放后的非Fast Bin块首站,作为缓存层,在下次malloc时会被整理进Small/Large Bin |
| Small Bin | 62 | 双向循环链表 (FIFO) | 存放固定大小的小块(<512/1024B),同一Bin内chunk大小全部相同。 |
| Large Bin | 63 | 双向循环链表 (排序) | 存放一定范围大小的大块(>=1024B),内部按size从大到小排序。 |
在较新版本的glibc中,还引入了Tcache(线程局部缓存),它在Bin机制之前生效,提供无锁的极速分配。
3.1 Fast Bin
Fast Bins是为了提高小内存块分配和释放效率而设计的一类特殊空闲链表,专门用于管理高频分配与释放的小型内存块。如果将一些较小的chunk释放之后发现存在与之相邻的空闲的chunk并将它们进行合并,那么当下一次再次申请相应大小的chunk时,就需要对chunk进行分割,这样就大大降低了堆的利用效率。
3.1.1 fast bin结构
fast bin的结构是单向链表,它采用LIFO(后进先出)算法,即最后释放的chunk内存块会被最先分配出去,默认包含10个bin,存储在malloc_state结构体中,通过fastbinsY数组进行维护。
fast bin管理从16字节到80字节(在64位系统下通常为32到128字节)的小型chunk,每个bin之间以8字节(32位系统)或16字节(64位系统)递增,由max_fast变量控制,默认支持最大的chunk的数据空间大小通常为64字节或128字节。
3.1.2 fast bin定义
Fast Bins存储在分配区(Arena)的主结构体malloc_state中:
struct malloc_state
{
/* ... 其他字段 ... */
/* FastbinsY 数组存储每个 fastbin 链表的头指针 */
mfastbinptr fastbinsY[NFASTBINS];
/* ... 其他字段 ... */
};
//指针类型定义:
typedef struct malloc_chunk *mchunkptr;
typedef struct malloc_chunk *mfastbinptr; /* 虽本质相同,但语义上用于单向链表 */
fast bin边界,即数量、大小定义:
/* Fast bins 的数量,通常定义为 10 */
#define NFASTBINS (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)
/* 默认的最大 fastbin 大小 (64位系统下通常为 128 字节) */
#ifndef DEFAULT_MXFAST
#define DEFAULT_MXFAST (64 * SIZE_SZ / 4)
#endif
/*Fast Bin理论上能够支持的最大物理尺寸上限,它决定了fastbinsY数组的大小,即硬上限*/
#define MAX_FAST_SIZE (80 * SIZE_SZ / 4)
/* 实际运行时的最大限制,存储在全局变量中,即运行时阈值,只有小于等于这个值的请求才会进入Fast Bin流程*/
static unsigned long global_max_fast;
状态位用于标识分配区中是否存在空闲的Fast Bin块:
/* 判断分配区是否有fast bin chunk,1表示没有,在malloc_state结构的flags中设置,第0位表示是否有fast chunks */
#define FASTCHUNKS_BIT (1U)
#define have_fastchunks(M) (((M)->flags & FASTCHUNKS_BIT) != 0)
#define set_fastchunks(M) catomic_or (&(M)->flags, FASTCHUNKS_BIT)
#define clear_fastchunks(M) catomic_and (&(M)->flags, ~FASTCHUNKS_BIT)
Fast Bin并没有直接存储在chunk自身的头部标志位里,而是通过分配区(Arena)状态标志以及相邻chunk的标志位来共同维护其特性。Fast Bin的标志位宏定义如下:
/* 定义在malloc_state(分配区)的flags字段的第一位 */
#define NONCONTIGUOUS_BIT (2U)
/* 检查分配区是否能返回连续的内存区域,用于快速判断当前分配区(Arena)的内存空间是否为连续增长模式,即检查NONCONTIGUOUS_BIT是否为 0 */
#define contiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) == 0)
/* 检查分配区是否为不连续模式,对应的反向检查宏 */
#define noncontiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) != 0)
/* 手动设置该标志位 */
#define set_noncontiguous(M) ((M)->flags |= NONCONTIGUOUS_BIT)
#define set_contiguous(M) ((M)->flags &= ~NONCONTIGUOUS_BIT)
/* 安全防护标志位,用于标识某个分配区(Arena)是否检测到结构性损坏,它是分配区flags字段中的第2位 */
#define ARENA_CORRUPTION_BIT (4U)
/* 辅助宏 */
#define arena_is_corrupt(A) (((A)->flags & ARENA_CORRUPTION_BIT))
#define set_arena_corrupt(A) ((A)->flags |= ARENA_CORRUPTION_BIT)
Fast bin中set_max_fast是一个用于初始化或重置Fast Bin最大可用阈值的管理宏:
/* 将 s 转换为内部对齐的大小,并设置 global_max_fast */
/* 只有Size ≤ global_max_fast的chunk才会进入Fast Bin逻辑 */
/* 如果将s设置为0,global_max_fast也会变为0。这意味着禁用所有Fast Bins,所有小块内存都会按Small Bin逻辑处理 */
/* 将全局变量global_max_fast设置为DEFAULT_MXFAST,也就是设置fast bins中chunk的最大值 */
#define set_max_fast(s) \
global_max_fast = (((s) == 0) \
? 0 \
: ((request2size (s) + CHUNK_OUTSIZE) & ~MALLOC_ALIGN_MASK))
Fast bin的索引与寻址宏用于将申请的内存大小(nb)映射到fastbinsY数组的具体位置:
//访问分配区av中下标为i的Fast Bin链表头指针。等价于av->fastbinsY[i]
#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[ idx ])
/* offset 2 to use otherwise unindexable first 2 bins */
// chunk size=2*size_sz*(2+idx)
// 计算给定大小sz的chunk在fastbinsY数组中的索引,逻辑通常为:(sz >> 3) - 2(32位)或 (sz >> 4) - 2(64位)
#define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
3.2 Small Bin
Small Bin主要是用于管理较小且固定大小的chunk空闲内存块,它介于高速缓存(fast bin)和用于大块内存的large bin之间,皆在平衡分配速度和内存利用率。
凡是小于MIN_LARGE_SIZE(通常 64 位下为1024字节)且不属于Fast Bin的空闲块,在整理后都会进入Small Bin。
3.2.1 基本规格与结构
malloc_state结构的bins数组中,Small Bins占据了索引从bin 2到bin 63的位置(bin[1]是unsorted bin,bin[64]之后是large bin),总共有62个small bins。每个Small Bin都是一个双向循环链表,且每个链表中存储的chunk大小都一致。
32位系统下用于管理小于512字节的chunk;64位系统下用于管理小于1024字节的chunk。同一个small bin里的所有chunk大小完全相同,相邻bin之间的chunk大小相差2个机器字长,即32位系统差8字节,64位系统差16字节。
3.2.2 分配与回收策略
small bin遵循FIFO(即先进先出)的原则,新的chunk插入到链表头部,分配时从链表尾部弹出,这保证了每个chunk都有均等的机会被重新分配。
用户free内存时,符合大小的chunk不会直接进入Small Bin,而是通常先进入Unsorted Bin作为缓冲,在随后的malloc操作中,系统遍历unsorted bin,发现大小合适的chunk会直接返回;如果不匹配但属于small bin大小范围的chunk会被移动到对应的small bin中。
small bin中的空闲chunk在释放或分配过程中是可以被合并。如果一个chunk的相邻chunk也是空闲的且不在fast bin中,它们会合并成一个更大的chunk以减少内存碎片。如果合并后的chunk变大,它可能会从Small Bin移动到Large Bin或Unsorted Bin。
当用户请求内存大小符合small bin范围时,small bin分配流程为:
- 直接查找:根据请求大小计算出对应的bin索引;
- 检查空闲:查看该索引的small bin链表是否为空;
- 获取内存:如果不为空,直接从链表尾部取出一个chunk返回给用户;
- 未命中处理:如果该bin为空,分配器不会立即去更大的small bin找,而是进入unsorted bin遍历逻辑,或者考虑切割更大的chunk;
3.2.3 结构定义
Small Bin并没有一个独立的struct smallbin结构体,而是作为分配区状态机malloc_state中bins 数组 的一部分来定义的。
Small Bin属于malloc_state(通常被称为arena)中的常规Bin。宏观结构定义如下:
struct malloc_state {
// ... 其他字段 (mutex, flags, fastbinsY)
/* 指向分配区的 top chunk */
mchunkptr top;
/* 最近一次切分后剩余的 chunk */
mchunkptr last_remainder;
/* 所有的 bins(包括 Unsorted, Small, Large)都存储在这个数组中 */
/* NBINS 通常为 128,索引从 1 开始 */
mchunkptr bins[NBINS * 2 - 2];
/* 标识 bin 是否为空的位图 */
unsigned int binmap[BINMAPSIZE];
// ...
};
bins数组实际上存储的是双向链表的头指针对(fd和bk)。由于每个Bin需要两个指针,数组大小被定义为NBINS * 2,Small Bin占据bins数组中索引为2到63的位置,索引1是Unsorted Bin,索引64及以后是Large Bin。
每一个Small Bin都是一个双向循环链表。在初始化时,bins[i] 的fd和bk指针都会指向bin[i]自身,表示链表为空。
Small Bin中链接的每一个内存块都是一个标准的malloc_chunk结构:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* 前一个 chunk 的大小 (如果空闲) */
INTERNAL_SIZE_T mchunk_size; /* 当前 chunk 的大小 */
struct malloc_chunk* fd; /* 双向链表:指向后一个 chunk */
struct malloc_chunk* bk; /* 双向链表:指向前一个 chunk */
/* 以下字段仅用于 Large Bin,在 Small Bin 中不被使用 */
struct malloc_chunk* fd_nextsize;
struct malloc_chunk* bk_nextsize;
};
Small Bin的操作依赖于一系列预定义的宏。这些宏主要用于范围判定、索引计算以及链表操作。
基础界限宏定义了Small Bin的数量和尺寸分界线:
// 规定了Small Bin的最大索引边界
#define NSMALLBINS 64
// 规定了Small Bin的宽度大小,32位系统是8字节,64位系统是16字节
#define SMALLBIN_WIDTH MALLOC_ALIGNMENT
// 是否需要对Small Bin的下标进行纠正
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ)
// 定义了进入Large Bin的最小阈值(通常64位下为1024字节)。凡是小于此值且不属于Fast Bin的,均受Small Bin宏逻辑管辖
#define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)
索引与范围判定宏用于在malloc流程中快速分流请求:
//判断chunk的大小是否在small bin范围内
#define in_smallbin_range(sz) \
((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)
// 根据chunk的大小得到small bin数组的索引,例如64位下,64字节对应的索引是4
#define smallbin_index(sz) \
((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) \
: (((unsigned) (sz)) >> 3)) + \
SMALLBIN_CORRECTION)
3.3 Large Bin
Large Bin专门负责管理大于等于1024字节(64位系统)或512字节(32位系统)的空闲内存块。与Small Bin最大的区别在于:其内部的chunk大小不固定,而是按范围划分。
3.3.1 基本规格与结构
malloc_state 结构的bins数组中,Large Bins占据了索引从bin 64到bin 126的位置,共有63个Large Bins。同一个Large Bin中的chunk按Size从大到小排序。如果Size相同,则按FIFO(先进先出)排序。
为了平衡查找效率,Large Bin的步进(Step)是阶梯式的:前32个步进64字节;后续16个步进512字节;后续8个步进4096字节;以此类推,最后一个Bin覆盖剩余所有超大尺寸。
为了在变长的链表中快速定位,malloc_chunk在Large Bin状态下会启用两个额外指针(跳表指针):
- fd_nextsize:指向链表中下一个比当前chunk小且Size不同的chunk;
- bk_nextsize:指向链表中上一个比当前chunk大且Size不同的chunk;
这两个指针构成了类似“跳表”的结构,使得分配器可以跳过大量相同大小的chunk,快速找到满足条件的最小Size节点。
3.3.2 分配与回收策略
Large Bin的分配与回收遵循“延迟放入”与“最佳匹配”原则。Large Bin的分配发生在Fast Bin、Small Bin和 Unsorted Bin均无法满足请求之后。
- 最佳适配 (Best-Fit):不同于Small Bin的精确匹配,Large Bin会在对应的Bin链表中寻找大于等于请求尺寸且最接近的一个chunk;
- 跳表加速:分配器利用fd_nextsize指针快速跳过相同大小的chunk;通常从该Bin中最小的chunk(通过bin->bk->bk_nextsize快速定位)反向查找,以尽快找到符合条件的最小块;
- 切分:找到合适的chunk后,如果该块比需求大很多(超过一个最小chunk的大小),分配器会将其一切为二,一部分返回给用户,剩余部分(Remainder)放入Unsorted Bin;
- FIFO倾向:如果链表中有多个相同大小的chunk,它会取走最先进入的那一个chunk;
Large Bin的“回收”并不是直接通过free完成的,而是一个整理过程:
- 缓冲阶段 (Unsorted Bin):当用户free一个大块内存时,它首先被放入Unsorted Bin。此时它并不在Large Bin中;
- 触发整理:在下一次调用malloc且无法直接从Fast/Small Bin获取内存时,分配器会遍历Unsorted Bin;
- 归类放入:遍历过程中,分配器发现该chunk属于Large Bin范围,并计算其对应的largebin_index;然后将chunk插入到对应的Large Bin链表中;
- 维护跳表:如果该size在链表中不存在,则需要更新fd_nextsize和bk_nextsize指针,确保链表依然按size降序排列;
- 合并:在放入之前或整理过程中,如果该chunk物理相邻的有其他空闲块,会先触发合并操作。合并后的新块若依然很大,则根据新size重新定位存放位置。
3.3.3 结构定义
Large Bin同Small Bin一样并不是一个独立的结构体,它在物理上复用了malloc_state 中的bins数组,但在逻辑上扩展了malloc_chunk结构的使用方式。Large Bin与Small Bin共享同一个bins数组,数组索bins[126]到bins[250](以fd/bk指针对形式存储,对应索引64到126):
struct malloc_state {
// ...
mchunkptr bins[NBINS * 2 - 2]; // 存储所有常规 bin 的 fd/bk 指针
unsigned int binmap[BINMAPSIZE]; // 快速判断某 Large Bin 是否为空的位图
// ...
};
Large Bins的核心元素结构是malloc_chunk。当一个chunk处于Large Bin中时,它会启用fd_nextsize和bk_nextsize指针,这是与Small Bin最本质的区别:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* 前一个 chunk 的大小 */
INTERNAL_SIZE_T mchunk_size; /* 当前 chunk 的大小 */
struct malloc_chunk* fd; /* 双向链表:指向后一个 chunk */
struct malloc_chunk* bk; /* 双向链表:指向前一个 chunk */
/* 仅在 Large Bin 中有效:跳表结构 */
struct malloc_chunk* fd_nextsize; /* 指向下一个比自己小的不同 size 的 chunk */
struct malloc_chunk* bk_nextsize; /* 指向上一个比自己大的不同 size 的 chunk */
};
Large Bin的物理形态是一个双层链表,无论是fd/bk还是nextsize指针,构成的都是双向循环链表:
- 第一层(fd/bk 链表):按照Size 降序排列,如果Size相同,则按FIFO顺序堆叠在同一个Size节点的后面;
- 第二层(nextsize 链表):只连接每个Size的第一个chunk,通过fd_nextsize快速跳过所有Size相同的节点,直接定位到下一个Size区间;
Large Bin宏定义主要集中在区间判定、索引计算(多级分段)以及跳表指针操作:
/* MIN_LARGE_SIZE 是界定 Small Bin 与 Large Bin 的核心分水岭。 */
/* 它的值并不是直接写死的数字,而是基于 Small Bin 的数量 和 对齐单位 计算得出的 */
/* 该宏主要用于 _int_malloc 中的分支跳转:
size < MIN_LARGE_SIZE:进入 Small Bin 处理逻辑(
精确匹配);
size >= MIN_LARGE_SIZE:进入 Large Bin 处理逻辑(
范围查找 + 跳表优化)
*/
/* 它标志着 bins 数组索引的分路。索引 < 64 的属于 Small Bin,索引 ≥ 64 的属于 Large Bin */
/* 32位系统下值为512,64位系统下值1024 */
#define MIN_LARGE_SIZE ((NSMALLBINS >> SMALLBIN_WIDTH_LOG) << SMALLBIN_WIDTH_LOG)
/* 用于在 malloc 或 free 流程中快速判断一个 chunk 的大小是否需要进入 Large Bin 处理逻辑。 */
/* sz:指的是经过对齐(aligned)后的 chunk size(包含元数据部分),将 sz 与 MIN_LARGE_SIZE 进行比较 */
#define in_largebin_range(sz) \
((unsigned long) (sz) >= (unsigned long) MIN_LARGE_SIZE)
//Large Bin 采用分级步进(阶梯式范围),通过多层位运算来定位 sz 所属的区间
//Large Bin 的索引计算遵循“Size 越大,Bin 覆盖范围越广”的原则。它将 62 个 Large Bin(索引 64-126)分成了 5 组(6 阶段)
//该宏通过检查 sz 的高位 bit 来快速分流
#define largebin_index_64(sz) \
(((((unsigned long) (sz)) >> 6) <= 48) ? 64 + (((unsigned long) (sz)) >> 6) : \
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) : \
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) : \
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) : \
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) : \
126)
//largebin_index_32(sz) 宏负责将 512 字节及以上的 chunk 大小映射到索引 64 到 126 的 Large Bin 中,
//32位系统下,第一个 large bin 的起始 chunk 大小为例,为 512 字节,那么 512>>6 = 8,所以其下标为 56+8=64
#define largebin_index_32(sz) \
(((((unsigned long) (sz)) >> 5) <= 31) ? 64 + (((unsigned long) (sz)) >> 5) : \
((((unsigned long) (sz)) >> 8) <= 15) ? 91 + (((unsigned long) (sz)) >> 8) : \
((((unsigned long) (sz)) >> 11) <= 7) ? 103 + (((unsigned long) (sz)) >> 11) : \
((((unsigned long) (sz)) >> 14) <= 3) ? 108 + (((unsigned long) (sz)) >> 14) : \
((((unsigned long) (sz)) >> 17) <= 1) ? 110 + (((unsigned long) (sz)) >> 17) : \
126)
//largebin_index_32_big(sz) 是一个专门为 32 位系统且对齐要求为 16 字节(即 MALLOC_ALIGNMENT == 16)的特殊环境设计的索引宏
//标准 largebin_index_32 第一级使用 >> 5(对应 32 字节步长),而 largebin_index_32_big 第一级使用 >> 6(对应 64 字节步长)。
//largebin_index_32_big 实际上是 ptmalloc 为了兼容性而设计的“变种”。它保证了在不同对齐约束下,Large Bin 的分级步进比例(如前 32 个 bin 是精细分段,后续逐渐变宽)能保持逻辑上的一致性
#define largebin_index_32_big(sz) \
(((((unsigned long) (sz)) >> 6) <= 45) ? 49 + (((unsigned long) (sz)) >> 6) : \
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) : \
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) : \
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) : \
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) : \
126)
3.4 Unsorted Bin
Unsorted Bin(索引为 1)扮演着“高速缓存”和“内存整理中转站”的角色。它是所有常规Bin中最特殊的一个。当一个chunk被free时(且不属于Fast Bin),它不会立即归位到对应的Small或Large Bin,而是先进入Unsorted Bin。下一次malloc时,分配器会优先检查Unsorted Bin,看其中的chunk能否直接满足需求,从而避免遍历复杂的Small/Large Bin。
Unsorted Bin链表中的chunk大小是混杂的。无论是属于Small Bin范围还是Large Bin范围的块,只要刚被释放且没进入Fast Bin,都会在这里并存。chunk仅按释放的时间顺序排列,不按大小排序。这种设计是为了实现快速的释放操作。
3.4.1 基本规格与结构
Unsorted Bin存储在malloc_state (Arena)的bins数组中,占据固定的索引位置bins[1]。Unsorted Bin作为一个单一的双向循环链表存在,且整个分配区只有一个 Unsorted Bin。
与Small Bin类似,Unsorted Bin使用malloc_chunk结构中的fd和bk指针:
- Dummy Header:bins[1]作为链表头(Head),不存储数据;
- 插入方式(LIFO倾向):新释放或合并后的chunk插入到链表的FRONT(靠近Header的一端);
- 遍历方式(FIFO逻辑):分配器在整理时,从链表的REAR(末尾,即bk指向的方向)开始逆向遍历;
Unsorted Bin中的Size是无限制的,链表中的chunk大小是混杂的。从最小的Small Bin尺寸到巨大的Large Bin尺寸都可能并存;chunk仅按释放的时间顺序排列,不按大小排序;malloc_state中的last_remainder指针通常指向Unsorted Bin中最近一次因切分(Split)而产生的剩余块,这有助于提高连续小内存分配的局部性。
3.4.2 分配与回收策略
Unsorted Bin的分配与回收策略遵循“延迟整理”与“FIFO遍历”原则,它是堆管理性能优化的核心缓冲层。
Unsorted Bin的分配逻辑发生在malloc流程中,当Fast Bin和Small Bin均无法满足请求时触发:
- 优先复用(Last Remainder 机制):如果请求的是Small Bin大小,分配器会先检查last_remainder(指向Unsorted Bin中最近一次切分剩下的块)。如果它的大小足够,直接切分并返回,剩下的部分继续留在Unsorted Bin。这保证了极佳的内存局部性;
- 迭代遍历与分发(核心逻辑):分配器从Unsorted Bin的末尾(Tail)开始逆向遍历(FIFO顺序):
- 1. 直接匹配:如果当前遍历到的chunk大小恰好等于请求大小,直接将其脱链并返回给用户;
- 2. 整理归位(Sorting):如果大小不匹配,分配器不会停止,而是顺手将这个 chunk 扔进它本该属于的 Small Bin 或 Large Bin:
- 若是Small Bin大小,插入对应Small Bin头部;
- 若是Large Bin大小,插入对应Large Bin并维护其跳表指针;
- 3. 切分利用:在遍历过程中,如果遇到一个比请求大的chunk,且它是唯一的或者满足特定条件,分配器会将其切分,一部分返回用户,剩余部分标记为新的last_remainder。
总结来说,回收时只进不出,只做物理合并,不做逻辑归类;分配时边找边扔,能用则用,不能用就将其“归位”到对应的Small/Large Bin中。
3.4.3 结构定义
Unsorted Bin的物理结构非常简单,但它在逻辑上是整个分配器的“核心枢纽”。它不具备Small/Large Bin那样的多链表阵列,而是作为一个单一的双向循环链表存在。
Unsorted Bin存储在malloc_state (Arena) 的bins数组中,占据固定的索引位置:
- 索引号:bins[0]和bins[1](物理上由这对指针组成fd和bk);
- 唯一性:每个 Arena(分配区)只有一个Unsorted Bin;
与Small Bin类似,Unsorted Bin使用malloc_chunk结构中的fd和bk指针:
- Dummy Header(链表头):bins数组中的对应项充当Head;
- 入队方式:新释放或合并后的chunk插入到链表的FRONT(靠近Header的一端);
- 遍历方式:分配器在整理时,从链表的REAR(末尾,即通过bk指针定位)开始逆向遍历,遵循FIFO(先进先出) 逻辑;
Unsorted Bin中的每一个节点都是标准的malloc_chunk:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; // 前一个物理块大小
INTERNAL_SIZE_T mchunk_size; // 当前块大小(含标记位)
struct malloc_chunk* fd; // 指向链表中更晚进入的 chunk
struct malloc_chunk* bk; // 指向链表中更早进入的 chunk
/* Unsorted Bin 不使用 fd_nextsize 和 bk_nextsize 字段 */
};
malloc_state中的last_remainder指针通常指向Unsorted Bin中最近一次因切分(Split)而产生的剩余块,这有助于提高连续分配小内存时的局部性。
Unsorted Bin的操作主要通过一些基础的Bin访问宏和专门的指针宏来实现。由于它在bins数组中的索引固定为 1,其宏定义相对直观:
//直接获取分配区 av 中 Unsorted Bin 的链表头指针。这是源码中最常用来引用Unsorted Bin的宏
#define unsorted_chunks(av) (bin_at (av, 1))
//用于获取当前分配区(Arena)允许的Fast Bin最大阈值 的宏。它直接决定了free的内存块是进入Fast Bin还是进入Unsorted Bin
//这是一个全局变量,记录了当前系统允许的Fast Bin最大尺寸
//get_max_fast() 的值是动态变化的,默认通常为 128 字节 (64位)
#define get_max_fast() (global_max_fast)
//通过判断 chunk 的大小,决定该 chunk 是该走 Small Bin 的精确匹配路径,还是走 Large Bin 的范围查找路径。
//输入参数 sz:经过对齐(Aligned)后的 chunk size(包含元数据头)。
//该宏在分配流程中起到了“分水岭”的作用:
#define in_smallbin_range(sz) \
((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)
4. 堆边界:Top Chunk
每个Arena的最高地址处,都有一个top chunk。当所有的Bin都无法满足分配请求时,分配器会从top chunk中切出一块内存返回给用户。如果top chunk也不够了,才会调用sbrk或mmap向内核申请更多空间。也就是说,Top Chunk是分配区(Arena)顶部的最后一个空闲内存块,它是堆内存分配的“最后一道防线”。
Top Chunk的结构与状态如下:
- inuse 标记:Top Chunk 的size字段中的PREV_INUSE (P) 位始终被设置为 1,以防止它被前面的空闲块合并;
- 不属于任何 Bin:Top Chunk是一个特殊的空闲块,它不被放在任何传统的Bin链表中;
- 内存回收限制:ptmalloc的内存回收机制依赖于Top Chunk。只有与Top Chunk相邻的内存块被释放并合并到Top Chunk后,这部分内存才有可能归还给操作系统;
5. ptmalloc内存分配逻辑
5.1 ptmalloc申请内存流程
ptmalloc2 中,malloc申请内存块的操作并非简单的向内核要空间,而是一套由快到慢、分层查找的复杂逻辑通过多级缓存(Bins)来平衡分配效率和内存碎片。malloc(size) 执行时的核心流程:
5.1.1 规格化 (Size Request)
首先,ptmalloc会对用户请求的大小进行对齐(32位下8字节,64位下16字节),并加上元数据(chunk header)的大小。如果请求太小,也会强制达到最小chunk尺寸。
5.1.2 寻找分配区 (Arena)
当线程调用malloc(size)时,分配器首先尝试获取一个Arena的锁:
- 如果当前线程已绑定一个Arena且未加锁,则直接使用;
- 如果发生竞争,分配器会尝试寻找或创建一个新的Non-main Arena,以实现多线程并行分配;
5.1.3 Tcache检索(最快路径,Glibc 2.26+)
这是现代Linux堆分配的首选路径:
首先查看对应大小的tcache单链表,每个线程私有,无需加锁,速度极快。如果有匹配合适大小的空闲块,直接并返回给用户。
5.1.4 Fastbins检索(极小块、快速路径)
如果Tcache没命中的情况下:
- 对象:针对小内存块(16-128字节左右);
- 逻辑:检查对应的fastbins链表。由于是LIFO(后进先出)的单链表,它同样很快;
- 特点:此阶段仍不进行块合并,以保持速度;
5.1.5 Small Bins检索(小块)
如果前面两项都没有命中:
- 对象:较小的块(<1024字节);
- 逻辑:在对应的双向链表中查找。如果命中,直接返回,返回块并标记为已使用;
5.1.6 整理与合并 (Consolidation)
如果上述“快缓存”都没有找到,ptmalloc开始进入整理模式:
- 它会把fastbins中的块取出,检查前后相邻块是否空闲,进行合并(Merge);
- 将合并后的块被放入Unsorted Bin中;
5.1.7 Unsorted Bin 遍历(核心分水岭)
这是ptmalloc最繁忙的阶段。它会遍历unsorted bin链表:
- 正合适:如果块大小正好等于需求,直接返回;
- 太大了:如果没找到精确匹配,就把遍历到的块按照大小“归位”到对应的Small Bins或Large Bins中;
5.1.8 Large Bins 检索(大块)
对于巨大的内存请求:
- 逻辑:在排序好的Large Bins中执行“最佳适配(Best-fit)”算法,寻找大于或等于请求大小的最小块;
- 切割: 如果找到的块太大,会将其切割,剩余部分放回Unsorted Bin。
5.1.9 Top Chunk 切割(最后屏障)
如果所有的 Bins 都没法满足需求:
- 逻辑:从Top Chunk(堆顶部的荒地)切出一块给用户;
- 扩容: 如果Top Chunk也不够,则触发系统调用:
- 主分配区: 调用sbrk()增加program break;
- 非主分配区: 调用mmap()增加子堆空间;
5.1.10 特殊处理:大对象 (mmap)
如果申请的内存超过了mmap 阈值(默认128KB),ptmalloc2会跳过上述所有Bin逻辑,直接调用mmap()向内核申请一块独立的内存。这块内存释放后会立刻归还给系统。
5.1.11 Chunk头部结构
申请到的内存块在堆中的真实映像:
- PREV_SIZE:前一个块的大小(如果前块空闲);
- SIZE:当前块大小 + 状态位(是否由mmap分配、前块是否在使用);
- User Data:malloc返回给你的地址指针指向这里;
5.2 ptmalloc释放内存流程
ptmalloc2中,free释放内存的过程并非简单地将空间还给内核,而是通过多级缓存和合并机制来减少系统调用的开销并防止内存碎片。Linux ptmalloc2的详细释放流程如下:
5.2.1 预检查与指针处理
- 指针检查: 如果传入的指针为NULL,直接返回;
- Chunk 转换: 将用户指针转换为指向malloc_chunk结构的指针(向前偏移8/16字节),读取其size字段;
5.2.2 TCache阶段 (Thread Local Cache)
- 首选路径: 为了极速响应,释放的块会优先尝试放入线程私有的TCache;
- 条件: 块大小在TCache范围内(通常
字节),且对应的bin链表未满(默认每组上限7个);
- 逻辑:放入后直接返回,不触发合并,无锁操作;
5.2.3 Fast Bins 阶段
- 条件: 如果TCache已满或不适用,且块大小max_fast (默认64/128字节);
- 逻辑:获取Arena锁,并将Chunk放入对应的Fast Bin(单向链表);
- 不合并:此时不进行块合并,即不修改相邻块的P位(PREV_INUSE);
5.2.4 块合并与 Unsorted Bin阶段:
对于不属于Fast Bin的中大型块,或者Fast Bin触发了整理时:
- 向前合并: 检查前一个相邻块。如果前块空闲(P位为0),利用prev_size找到前块头部并将其合并;
- 向后合并: 检查后一个相邻块。如果后块不是Top Chunk且处于空闲状态,将其从Bin中摘除并合并;
- 放入 Unsorted Bin: 合并后的新大块被放入Unsorted Bin(双向链表),作为下一轮分配的缓冲区;
5.2.5 Top Chunk 合并与堆收缩
- 合并Top Chunk: 如果释放的块(或合并后的块)紧邻堆顶的Top Chunk,则直接并入Top Chunk;
- 触发收缩: 如果Top Chunk的大小超过了收缩阈值(默认128KB),ptmalloc2可能会通过sbrk或munmap将部分内存归还给操作系统;
5.2.6 特殊情况:mmap块
如果块头部标记显示它是通过mmap分配的(IS_MMAPPED位为 1),则直接调用内核的munmap()销毁该内存映射,空间立即彻底归还系统。
5.2.7 内存释放总结
| 块类型 | 存储位置 | 是否合并 | 归还系统 |
| 微小块 | TCache/Fast Bins | 否 | 几乎不还 |
| 普通块 | Unsorted Bin | 是(前后合并) | 仅在合并到 Top Chunk 且超限时 |
| 巨大块(>128KB) | 不进入Bin | N/A | 立即归还 |
1. 一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。