【底层机制】malloc 在实现时为什么要对大小内存采取不同策略?

时间:2025-09-05 15:15:02来源:互联网

下面小编就为大家分享一篇【底层机制】malloc 在实现时为什么要对大小内存采取不同策略?,具有很好的参考价值,希望对大家有所帮助。

malloc 在底层区分小内存和大内存分配,主要是为了在性能内存利用率碎片控制这几个关键目标上取得最佳平衡。

这种区分是多年实践和优化的结果,其主要原因可以归纳为以下几点:


1. 应对不同分配模式的需求

程序的内存分配请求通常呈现出一种“二八定律”或类似模式:

  • 频繁分配和释放的都是小块内存(如对象、结构体、字符串等)。对这些操作,速度是首要目标
  • 偶尔才会分配大块内存(如大的缓冲区、数组、图像数据等)。对这些操作,避免内存浪费和方便管理更为重要

为这两种截然不同的模式采用同一种分配策略是低效的。因此,malloc 采用了分级分配的策略。

2. 减少内存碎片(Fragmentation)

内存碎片是内存分配器的天敌,分为内部碎片和外部碎片。

  • 内部碎片:分配器分配给程序的内存块比程序实际请求的大,这中间浪费的空间就是内部碎片。

  • 外部碎片:空闲内存被分散成许多不连续的小块,导致无法满足较大的分配请求,即使总空闲空间足够。

  • 对于小内存:分配器(如 glibc 的 ptmalloc)会预先通过 brk/sbrk 申请一大块内存(称为 HeapArena),并将其划分为各种固定大小的内存池(也称为 bins,例如 16字节、24字节、32字节...的桶)。

    • 当程序申请小块内存时,分配器会快速找到一个合适大小的桶,从中分配一块。
    • 释放时,内存块回到对应的桶中。
    • 好处:由于大小固定,分配和释放速度极快。并且能有效减少外部碎片,因为只有相同大小的内存块会在池子中交换。
  • 对于大内存:使用 mmap 单独映射一块内存。

    • 这块内存的大小正好等于请求的大小(加上一些元数据开销)。
    • 好处:分配和释放都非常直接。释放时通过 munmap 将整块内存直接归还给操作系统,完全避免了这块内存区域的碎片问题(无论是内部还是外部碎片)。

3. 性能优化

  • 小内存分配:在预先分配的内存池中操作,主要是在用户态进行链表操作,速度非常快。并且由于操作的是连续的内存区域,缓存局部性(Cache Locality)更好
  • 大内存分配:虽然 mmap 系统调用有一定开销,但大内存分配本身就不频繁,这种开销是可接受的。更重要的是,munmap 释放大内存后,能立刻将物理页归还给操作系统,减轻系统整体内存压力。

4. 降低系统调用开销

系统调用(如 brkmmap)需要从用户态切换到内核态,开销很大。

  • 通过 brk 一次性扩展堆空间来服务大量的小内存请求,可以摊薄(Amortize) 每次分配的系统调用开销。成百上千次小分配可能只源于一两次 brk 调用。
  • 对于大内存,由于其不频繁,即使每次分配都调用一次 mmap,总体开销也是可控的。

5. 便于内存归还操作系统

这是一个关键但常被忽略的点。

  • brk 分配的堆内存:释放小内存块时,它只是回到分配器的空闲列表中,并不会立即触发收缩堆的操作(brk 系统调用减少program break)。因为分配器预期程序很快就会再次申请同样大小的内存。收缩堆是一个昂贵的操作,而且可能只在堆顶部的内存全部空闲时才能进行。因此,通过 brk 分配的内存很难彻底还给操作系统。
  • mmap 分配的大内存:由于是独立映射的,free 这类内存时,可以立即调用 munmap干净利落地将整块内存连同理物理页一起归还给操作系统。这对于长时间运行的程序(如守护进程、服务端程序)非常重要,可以防止它们的内存占用(RSS)无限增长。

总结对比

特性小内存分配 (通过 brk)大内存分配 (通过 mmap)
设计目标速度低碎片化易于管理避免碎片易于归还
分配策略从预先分配的大小分类的内存池(bins) 中获取直接从操作系统独立映射一块所需大小的内存
碎片主要产生内部碎片,有效控制外部碎片几乎无碎片(分配和释放都是整块进行)
系统调用开销被摊薄(多次分配对应一次brk每次分配/释放都对应一次 mmap/munmap
归还系统困难且延迟,只在堆顶空闲时才能收缩立即且彻底free 即调用 munmap
适用场景频繁、大量的小块内存请求不频繁的大块内存请求

这种“大小分离”的设计是现代内存分配器(如 glibc 的 ptmalloc)高效工作的基石。它聪明地针对不同规模的内存请求采用了最合适的策略,从而在整体上提供了优异的性能。

本站部分内容转载自互联网,如果有网站内容侵犯了您的权益,可直接联系我们删除,感谢支持!