malloc内存分配原理 代码中调用的malloc是 glibcGNU C 库提供的封装函数其底层采用的是ptmalloc2或 jemalloc/tcmalloc 等替代品。它的核心目标是尽量通过复用已释放的内存来避免陷入内核因为系统调用陷入内核的开销很大。 当调用 malloc(size)到拿到真正的物理内存中间经历了层层递进的复杂流程。过程按用户态库函数、系统调用和内核态物理内存管理三个层次来拆解第一层用户态 —— glibc 的malloc分配器当调用malloc(size)时ptmalloc2 会按以下优先级尝试分配1、检查线程本地缓存 (tcache)对于小内存默认最大 1032 字节分配器会首先查看当前线程专属的tcache。如果有空闲块直接返回这是最快的路径完全无锁。本地缓存 (tcache)是线程私有、无锁访问。你可以把它看作是每个线程自己专属的小仓库用于缓存刚刚释放的小块内存。其设计原则如下1极致快速路径每个线程在分配或释放小内存时会优先操作自己的 tcache。因为其他线程无法访问该 tcache所以整个过程完全不需要加锁lock-free大大减少了线程间争夺全局锁的开销。2数据结构每个线程的 tcache 由一个管理结构tcache_perthread_struct和多个链表entries构成。它按照内存大小分类bin每个大小类别的 bin 默认最多缓存7 个空闲内存块总数限制在 64 个 bin 以内。3缓存范围tcache 主要处理小块内存的分配通常是24 到 1032 字节64位系统范围内的请求。4作为“L1 缓存”这个设计和 CPU 的多级缓存思想类似。tcache 是离线程最近的“L1 缓存”访问最快如果 tcache 中没有合适的内存分配器才会去访问需要加锁的全局 Arena可以看作“L2 缓存”。2、检查 Fastbins如果tcache没有对于更小的内存默认 80 字节会去fastbins查找。这是另一个针对极小对象的高速缓存。Fastbins相当于是通用内存分配器的二级缓存它设计原则如下1全局共享Fastbins是全局的“二级缓存”归整个进程所有。因此当线程访问 fastbins 时需要加锁使用互斥锁以防止多线程同时修改。2次优先级当tcache中没有空闲块时分配器才会去fastbins以及后续的bins中查找。3缓存范围它只缓存极小内存块默认 80 字节使用更简单的单向链表结构操作速度也很快。两者是联动的形成了“分配优先用tcache回收优先填tcache”的机制分配内存时线程malloc→tcache(有则返回) →fastbins(有则返回) → 更慢的bins或系统调用。释放内存时线程free→tcache(若未满则放入) → (若已满则放入)fastbins。3、检查 Bins (Small/Large)如果以上都没有分配器会去bins回收站中查找。它会按大小small bins或特定算法large bins寻找一个合适的空闲块。如果找到会将其从bins中取出分割出需要的部分并返回。如果说tcache和fastbins是“高速缓存”那么bins就是管理绝大部分空闲内存的“核心仓库”。1在ptmalloc2中所有内存块无论是正在使用的还是空闲的都由一个统一的结构体malloc_chunk来管理。struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* 前一个 chunk 的大小如果前一个空闲则使用 */ INTERNAL_SIZE_T mchunk_size; /* 当前 chunk 的大小包含头部且末位是标志位 */ struct malloc_chunk* fd; /* 前向指针 (空闲时使用, 指向链表中下一个) */ struct malloc_chunk* bk; /* 后向指针 (空闲时使用, 指向链表中前一个) */ struct malloc_chunk* fd_nextsize; /* 仅 large bin 使用用于加速查找 */ struct malloc_chunk* bk_nextsize; /* 仅 large bin 使用 */ };2ptmalloc2的空闲内存被组织在bin数组中共 128 个桶bins每个桶管理着不同大小或范围的内存块。bin 类型索引位置数据结构管理算法核心作用Unsorted Bin索引 1双向循环链表FIFO回收缓冲区。释放的内存会先放在这里等待下次分配时快速重用减少合并开销。Small Bins索引 2 - 63双向循环链表精确匹配(FIFO)每个索引对应一个固定大小的内存块如 32B、48B。分配速度很快。Large Bins索引 64 - 126双向循环链表最佳适配每个索引对应一个大小范围。在范围内寻找最合适的内存块分配成本最高。小内存的精确匹配Small BinsSmall Bins 的管理非常严格每个 bin 索引固定对应一个chunk大小。查找算法当分配小内存时ptmalloc2会通过公式size / 16直接计算出对应的 bin 索引。分配算法由于链表中的每个chunk大小完全一样它直接从链表头取出第一个空闲块即可是典型的FIFO先进先出策略。大内存的最佳适配Large BinsLarge Bins 的管理要复杂得多其目标是最小化内存浪费。查找算法当分配大内存时它会从对应的 bin 开始查找尝试找到最小且能满足需求的空闲块Best-fit。数据结构优化为了加速查找它使用了fd_nextsize和bk_nextsize指针将不同大小的chunk按大小排序形成一个额外的索引链避免遍历所有节点。3Bins完整生命周期分配与回收的核心算法分配内存 (malloc)程序会先检查 Unsorted Bin尝试找到一个合适的内存块。如果找到就直接用否则进入下一步。再查 Small / Large BinsSmall Bin直接索引到对应的桶取走第一个空闲块。Large Bin通过 best-fit 算法在对应的 bin 范围中查找最小合适块。 关键操作拆分 (Split)如果找到的空闲 chunk 比请求的大小大很多例如请求 24 字节但找到了一个 512 字节的块分配器会进行拆分 取出需要的部分如 24 字节返回给用户。 将剩下的部分如 488 字节重新打包成一个新的空闲 chunk并挂回到 Unsorted Bin 中以便后续利用。回收内存 (free)内存释放的流程相对简单核心在于合并以对抗内存碎片。如果用户在 tcache 或 fastbins 范围内直接放入相应的缓存流程结束。否则进入 Unsorted Bin内存块会被放入 Unsorted Bin 这个缓冲区。关键操作向后或向前合并。在放入前ptmalloc2会检查物理上相邻的前后chunk是否空闲。如果是它会将这些相邻的空闲chunk合并成一个大块以应对未来更大的内存请求。这一步是减少内存碎片最关键的机制。定期整理如果 Unsorted Bin 中的空闲块积累过多分配器会将其整理并移动到对应的 Small/Large Bins 中完成最终的分类。4、向内核申请 (系统调用)如果bins中也没有合适的空闲块说明用户程序确实需要更多的内存这时malloc才会调用brk或mmap系统调用向操作系统内核发起内存请求。第二层系统调用 —— 陷入内核态当用户态分配器无法满足请求时会触发系统调用将控制权从用户态切换到内核态。在 x86_64 架构下这通常通过syscall指令完成并伴随上下文切换的开销。Linux 提供了两种主要的内存申请系统调用malloc会根据请求的大小进行选择brk/sbrk系统调用用于小内存分配默认阈值 128KB。brk做的事情非常简单粗暴就是移动堆Heap的结束指针program break。它相当于对内核说“请把我进程的堆空间扩大 1024 字节。”mmap系统调用用于大内存分配 128KB。mmap会直接在进程的虚拟地址空间中创建一块独立的匿名内存映射MAP_ANONYMOUS | MAP_PRIVATE这段内存通常不在堆区而是位于文件映射区Loading Region。第三层内核态 —— 物理内存的管理这是最底层的步骤。内核接收到brk或mmap请求后开始分配真正的物理内存。1. 处理brk系统调用内核找到当前进程的内存描述符mm_struct修改其brk字段堆的结束地址。这里要注意brk仅仅是在虚拟地址空间层面“预留”了一块地址范围并不会立即分配物理内存页。2. 处理mmap系统调用内核在当前进程的虚拟内存区域VMA, Virtual Memory Area链表中找到一块合适地址对齐、长度匹配的未映射区域。如果找到就创建一个新的vm_area_struct结构体将其标记为匿名映射Anonymous Mapping并插入到进程的 VMA 链表中。3. 缺页中断 (Page Fault) —— 真正的物理内存分配上面两步只是在虚拟地址空间做好了“规划”实际的物理内存页框还没有分配。当你的程序第一次尝试读写这块新内存时例如memset(p, 0, 1024)CPU 的 MMU内存管理单元会触发缺页中断。此时内核的缺页中断处理程序会接管查找 VMA根据发生缺页的虚拟地址查找进程的 VMA确认这次访问是合法的。物理页分配从伙伴系统Buddy System中分配一个或多个空闲的物理页框通常为 4KB。如果是mmap的大内存可能还涉及更高级的分配策略。建立页表映射在进程的页表中建立虚拟地址到物理地址的映射关系。返回CPU 重新执行引发缺页的指令此时物理内存已经就绪程序可以正常读写。总结一览表层次模块核心操作备注用户态malloc(ptmalloc2)从tcache/fastbins/bins复用内存快速路径通常无锁用户态 → 内核系统调用 (syscall)brk(小内存) 或mmap(大内存)上下文切换有一定开销内核态 (虚拟内存)VMA 管理修改堆边界 (brk) 或创建新 VMA (mmap)仅在虚拟地址空间预留位置内核态 (物理内存)缺页中断 伙伴系统分配物理页框并建立页表映射按需分配此时才真正消耗物理内存这个设计的精妙之处在于它通过延迟分配直到第一次访问才分配物理内存和用户态缓存尽量复用已释放的内存在内存使用效率和性能之间取得了非常好的平衡。