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