4-2 内存池的设计与实现
#1.内存碎片 在C++内存管理一章中介绍到,内存分区中堆区由C++进程主动申请和释放,堆内存的管理具有内存申请大小任意、即时分配和释放的特点。因此,频繁的堆内存申请与释放会产生空闲的内存块以小且不连续方式出现在任意位置,这些不可用的空闲内存被称作内存碎片。
1.1 内碎片
操作系统的内存分配方式一般起始于可被4、8或16整除的内存位置(视处理器体系结构而定),内存分配算法仅能把预定大小的内存块分配。因此当某个程序申请一个23字节的内存块时,因为没有刚刚好23字节的内存块,所以可能会获得32字节、64字节等更大一点的字节空间。将程序申请所需内存大小增加到相对内存块而产生的多余空间就叫内碎片。
内碎片是已经被分配出去(能明确指出属于哪个进程)的内存空间大于请求所需的内存空间,这部分多分配的不能被利用的内存空间就是内碎片。
1.2 外碎片
外部碎片是当已分配内存块之间出现未被使用的较小内存块时,这部分较小内存块还没有分配出去(不属于任何进程),但是由于大小而无法分配给申请内存空间的内存空闲块。
如下图所示:有8个内存块被分配给了3个变量,其中变量A分配3个内存块,变量B分配1个内存块,变量C分配4个内存块。此时,变量B生命周期结束,内存空间被释放。但如果后续的所有进程运行过程中,存在申请的内存块数量为1个,那么可以在变量A和变量B之间的这个内存块会被分配。但如果申请的内存块数量始终都大于1块,那么变量A和变量B之间的这个内存块永远得不到使用,也就是外碎片。
2.C++内存池设计与实现
2.1 内存池设计原理
内存池的设计原理可归纳为一次性或较少次的从系统中申请足够大小的内存空间,并自定义内存分块、申请与释放的策略,由程序管理该程序所有变量的内存分配与释放。内存池可以在一定程度上提升内存资源管理的效率(减少堆内存申请与释放),并极大程度的避免内存碎片的产生。因此,对于7*24小时运行的程序原则上应该使用内存池进行内存管理。
内存池的首要设计目标为:(1)简单;(2)高效;(3)针对性强;
其概念模型如下图所示:
- 内存池的设计也是将内存分块,内存块单位大小可按业务调整。内存块通过链式连接,不同单位大小的内存块相连组成了多个内存块链表。
- 内存池在进程启动时,首先向操作系统申请一大片内存空间,并初始化为设计好的多个内存块链表。
- 在程序运行过程中,内存池接收到分配内存的请求,首先判断内存申请的大小,选择大于等于申请内存空间的内存块链表进行快速分配并响应内存申请。
- 内存池接收到内存释放请求时,根据释放内存所在地址,快速判断所在的内存块链表,将该内存块清空并恢复到链表中等待被分配(此时需要内存块头部存储其所归属的内存链表信息)。
- 因此,链式内存池的设计最重要的优点在于高效和简单,在内存分配时不需要做过多的计算应该分配在哪里,而是直接查找对应的内存链,再查找链内的空闲块分配即可。内存释放时,直接将内存块清空并将内存块恢复到链表中。
2.2 内存池的意义
程序在运行过程中不可避免的会产生内存碎片,对于后台常驻的server进程要想长期运行一周、一个月或者更久,由系统进行内存管理会产生一系列不可控制的情况发生。
内存池应该被设计成与服务相匹配的内存管理机制,使得程序员可以有能力管理内存,避免内存碎片的产生,使程序长期稳定、高效运行。
2.3 内存池实现步骤
- Step1: 编写内存块头部类,用于封装内存块的基本属性,相当于实现链表的结点
class MemoryAlloc; // 内存池类前向声明
class MemoryBlock
{
public:
MemoryBlock()
{
pAlloc = nullptr;
pNext = nullptr;
}
~MemoryBlock() {}
public:
// 内存块编号
int nID;
// 引用次数
int nRef;
// 所属内存池的指针 相当于所属链表的指针
MemoryAlloc* pAlloc;
// 下一块位置
MemoryBlock* pNext;
// 是否在内存池中
bool bPool;
};
- Step2: 使用模板类编写内存池类,便于在声明内存池对象时,利用模板初始化成员变量的做法。
template<size_t nBlockSize, size_t nSize>
class MemoryAlloc
{
protected:
MemoryBlock* _pHead; // 内存块头部结点指针
size_t _nBlockSize; // 内存块的单元大小
size_t _nSize; // 内存块的数量
char* _pBuff; // 内存池首地址
public:
MemoryAlloc() // 构造函数初始化
{
_pBuff = nullptr;
_pHead = nullptr;
_nBlockSize = nBlockSize;
_nSize = nSize;
}
~MemoryAlloc()
{
if(_pBuff) // 析构函数释放资源
{
free(_pBuff);
}
_pBuff = nullptr;
_pHead = nullptr;
_nBlockSize = 0;
_nSize = 0;
}
// 初始化内存池
void initMemory();
// 分配内存
void* allocMemory(size_t nSize);
// 释放内存
void freeMemory(void* pMem);
};
内存池类的成员变量包括内存块的头结点指针、内存块的单元大小(16字节、32字节、64字节等等)、内存块的数量等,相当于链表中的头结点指针、链表结点的大小和结点数量。内存池类的成员方法包括初始化方法、分配内存方法和释放内存方法。接下来依次实现这三个方法:
- 1.实现初始化内存池 initMemory()
void MemoryAlloc::initMemory()
{
if (_pBuff)
{
return;
}
// 为内存池向系统申请内存,此处计算出该内存池共需要多大的内存资源
// 内存池的概念图表达了:内存块的整体由头部信息+实际内存块空间组成,其中_nBlockSize是内存块空间,MemoryBlock是内存块的头部信息
// 因此,一个内存块所需要的整体内存空间=_nBlockSize + sizeof(MemoryBlock)
size_t size = _nSize * (_nBlockSize + sizeof(MemoryBlock));
// 利用malloc向系统申请该内存池需要的全部资源
_pBuff = (char*)malloc(size);
// 申请到系统内存资源后,将其切割成多个内存块,并按链表的方式管理
// 首先初始化头结点
_pHead = (MemoryBlock*)_pBuff; // 头结点指针等于内存池首地址
_pHead->nID = 0;
_pHead->bPool = true;
_pHead->nRef = 0;
_pHead->pAlloc = this; // 内存块所属的内存池指针赋值
_pHead->pNext = nullptr;
// 循环nSize-1次,构造nSize-1个内存块节点
for (int i = 1; i < _nSize; i++)
{
// 计算出下一个内存块的地址
MemoryBlock* pTemp = (MemoryBlock*)((char*)_pBuff + (sizeof(MemoryBlock) + _nBlockSize) * i);
// 赋值内存块的属性
pTemp->bPool = true;
pTemp->nID = i;
pTemp->nRef = 0;
pTemp->pAlloc = this;
pTemp->pNext = nullptr;
// 将前序结点的next指向当前节点
_pHead->pNext = pTemp;
_pHead = pTemp;
}
_pHead = (MemoryBlock*)_pBuff; // 头部回到内存池首地址
}
-
- 分配内存方法allocMemory(size_t nSize)实现,该方法的参数是需要分配的内存大小,返回值为分配成功的内存地址
void* MemoryAlloc::allo
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> C++工程师面试真题解析! </p> <p> 邀请头部大厂创作者<a href="https://www.nowcoder.com/profile/73627192" target="_blank">@Evila</a> 及牛客教研共同打磨 </p> <p> 助力程序员的求职! </p>