内存池
网盘课
malloc和free对小块内存频繁管理效率低
对比传统 malloc/free
传统的 malloc/free
会导致严重的碎片问题:
- 外部碎片:频繁分配 / 释放不同大小的内存会在堆中留下许多无法利用的小空闲块
- 内部碎片:每次分配都会有元数据开销(如块大小、标志位等),且可能因对齐产生额外空间浪费
而自由链表通过分类管理、复用机制和批量操作,显著减少了这两种碎片。
总结
自由链表减少内存碎片的核心在于:
- 固定大小块:避免随机大小分配导致的间隙
- 即时复用:释放的内存立即被回收再利用
- 批量分配:减少与操作系统的交互,保持内存连续性
- 对齐优化:消除因对齐需求产生的额外开销
设计思路
1. 减少系统调用开销
- 传统的
malloc/free
或new/delete
涉及系统调用,需要从用户态切换到内核态,成本较高。 - 内存池通过一次性分配大块内存(如几 KB 或几 MB),将多次小内存分配转化为一次大内存分配,减少系统调用次数。
2. 降低内存碎片
- 频繁分配和释放不同大小的内存会导致内存碎片化(外部碎片和内部碎片)。
- 内存池通过固定大小块分配和对象复用机制,避免产生无法利用的小空闲块。
3. 提高分配效率
- 系统级分配器需要维护复杂的内存状态信息(如空闲链表、位图等),查找合适的内存块耗时较长。
- 内存池采用简单的数据结构(如链表、数组)管理内存,分配和释放操作通常是 O (1) 时间复杂度。
4. 自定义内存策略
- 针对特定场景(如频繁创建销毁小对象)优化分配策略。
- 支持内存预分配、对象池化、批量释放等特性。
基本原理
1. 预分配内存
-
内存池初始化时,向操作系统申请一块连续的内存区域(称为内存池块或内存池 Chunk)。
-
这块内存通常比单次请求的内存大得多,例如:
cpp
char* memory_pool = new char[1024 * 1024]; // 预分配1MB内存
2. 内存块管理
- 将预分配的内存划分为多个内存块(Memory Block),根据分配策略不同,块大小可以是:
- 固定大小(如 8B、16B、32B 等):适合管理相同类型的小对象。
- 可变大小:根据实际需求动态划分,但管理复杂度较高。
3. 分配器接口
-
提供自定义的
allocate()
和deallocate()
函数,替代malloc
和free
:cpp
void* allocate(size_t size); // 从内存池分配指定大小的内存 void deallocate(void* ptr); // 将内存块返回给内存池
4. 空闲列表(Free List)
- 最常见的实现方式是维护一个空闲内存块链表:
- 每个空闲块的头部包含指向下一个空闲块的指针。
- 分配时直接从链表头部取出一块,释放时将块插入链表头部。
- 操作复杂度为 O (1)。
5. 内存池耗尽处理
- 当预分配的内存用完时,有两种处理方式:
- 扩展内存池:向操作系统申请新的内存块,并添加到内存池管理。
- 回退到系统分配器:调用
malloc
分配内存,但可能破坏内存池的优化效果。
SGI STL内存池实现细节
SGI STL 的内存池采用两级分配器架构:
- 一级分配器:处理大块内存(>128B),直接调用
malloc
和free
。 - 二级分配器:处理小块内存(≤128B),使用自由链表(Free List)管理。
自由链表的组织结构
-
将内存块按 8 字节对齐分组,共 16 个链表:
plaintext
索引0:管理8B的内存块 索引1:管理16B的内存块 ... 索引15:管理128B的内存块
-
每个链表的节点结构为:
cpp
union _Obj { union _Obj* _M_free_list_link; // 空闲时作为链表指针 char _M_client_data[1]; // 分配后作为用户数据 };
分配流程
- 用户请求
n
字节内存。 - 计算
n
对应的链表索引i
(通过_S_freelist_index(n)
)。 - 若链表
i
非空,取出头部节点分配;否则:- 调用
refill()
从内存池取出 20 个新节点,1 个返回用户,19 个放入链表。 - 若内存池不足,调用
chunk_alloc()
分配新的内存池块(通常是 2^n 大小)。
- 调用
释放流程
- 根据指针地址计算所属链表索引
i
。 - 将节点插入链表
i
的头部。
两个辅助函数
将_bytes上调至邻近的8的倍数
enum{_ALIGN = 8};
static size_t _S_round_up(size_t _bytes)
{
return (((_bytes)+(size_t)_ALIGN-1)&~((size_t)_ALIGN-1));
}
//结果是1~8=>8
//9~16=>16...
//ALIGN-1 = 0111
//~(ALIGN-1)=11111111 11111111 11111111 11111000
//(_bytes)+(size_t)_ALIGN-1)当_bytes>0时会产生进位,&与运算后只保留进位
返回_bytes大小的chunk位于free-list中的下标
_S_freelist_index
n>max_bytes用一级空间配置器即malloc分配
n<max_bytes用内存池分配
自由链表的增删改要保证线程安全,加锁。在出代码块时锁析构
![[Pasted image 20250514003919.png]]
_S_refill()
头为空
关键概念
1. 基本定义
(1) Chunk(大块内存)
- 定义:从操作系统一次性分配的连续大块内存,通常是几 KB 或更大。
- 用途:作为内存池的 “原材料”,被分割为多个小的
Block
。 - 管理:由内存池直接管理,多个
Chunk
可通过链表连接。
(2) Block(小块内存)
- 定义:
Chunk
被分割后的固定大小的内存单元,大小通常是 8 的倍数(如 8B、16B、...、128B)。 - 用途:直接分配给用户或由自由链表管理。
- 管理:通过自由链表组织,每个链表管理一种固定大小的
Block
。
2. 关键区别
对比项 | Chunk | Block |
---|---|---|
大小 | 通常几 KB(如 8KB、16KB) | 固定大小(8B、16B、...、128B) |
分配对象 | 操作系统(通过 malloc ) |
用户(通过内存池的 allocate ) |
分配频率 | 低频(链表耗尽时才分配新 Chunk) | 高频(用户每次请求内存) |
管理方式 | 由内存池直接管理(Chunk 链表) | 由自由链表管理(按大小分类) |
生命周期 | 长期存在,直到内存池销毁 | 短期存在,频繁分配 / 释放 |
3. 自由链表存储的内容
自由链表存储的是 Block
的地址,而非 Chunk
的地址。具体来说:
- 每个自由链表管理同一大小的多个
Block
。 - 链表中的每个节点是一个
Block
,其前 4/8 字节(取决于指针大小)存储指向下一个Block
的指针。 - 当链表为空时,内存池从
Chunk
中切分新的Block
并加入链表。
_S_refill()
的作用
-
补充内存池的空闲块
当内存池中某个特定大小(size
)的内存块被耗尽时,_S_refill()
会从系统中申请一大块内存,将其分割成多个size
大小的块,并将这些块加入到内存池的空闲链表中,供后续分配请求使用。 -
减少系统调用的频率
通过一次性申请多个内存块(如n
个),避免频繁调用malloc
或mmap
,提升内存分配效率。 -
支持多层级内存池管理
SGI STL 的内存池针对不同大小的内存块(如 8B、16B、32B 等)维护独立的空闲链表。_S_refill()
会根据请求的size
选择对应的链表进行补充。
与 _S_chunk_alloc()
的关系
_S_refill()
通常依赖于 _S_chunk_alloc()
函数来完成实际的内存分配:
_S_chunk_alloc(size_t bytes)
:从系统申请bytes
大小的内存块,并返回其地址。_S_refill()
会调用_S_chunk_alloc
获取大块内存,再进行分割。
当新配内存占了没分配完的备用内存且nobjs修改为1则不需要refill链接。否则把备用块插入对应大小的链表中
![[Pasted image 20250515200045.png]]
SGI优点
1.对于每一个字节数的chunk块分配,都是给出一部分进行使用,另一部分备用,备用块可以给当前字节数使用,也可以给其他字节数使用
2.对于备用内存池划分完后剩余的很小块内存,再次分配时会把小内存块分配出去,不浪费内存池
3.当指定字节数内存分配失败以后,有一个异常处理的过程,bytes到28字节所有chunk块查看,如果某个字节数有空闲chunk块,会借出使用