本文共 3783 字,大约阅读时间需要 12 分钟。
本文转载自:
通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:
(1)因为我们申请的内存块大小不定,申请和释放时间也不相同。 所以这个堆(连续的整个内存块) 会形成零零散散的情况(称为内存碎片)。当频繁使用时会造成大量的内存碎片并进而降低性能;
(2)在堆中分配内存时,需要查询查询已分配链表和空闲块链表,查询这两张表需要时间,所以如果申请和释放堆内存比较频繁的话,会比较浪费时间; (3)当我们调用malloc或new时,会执行(brk、mmap和munmap)等系统调用,调用的成本是非常非常高的;内存池技术因为其对内存管理有着显著的优点,在各大项目中广泛应用,备受推崇。但是,通用的内存管理机制要考虑很多复杂的具体情况,如多线程安全等,难以对算法做有效的优化,所以,在一些特殊场合,实现特定应用环境的内存池在一定程度上能够提高内存管理的效率。经典内存池技术,是一种用于分配大量大小相同的小对象的技术。通过该技术可以极大加快内存分配/释放过程。既然是针对特定对象的内存池,所以内存池一般设置为类模板,根据不同的对象来进行实例化。
(1)先申请一块连续的内存空间,该段内存空间能够容纳一定数量的对象; (2)每个对象连同一个指向下一个对象的指针一起构成一个内存节点(Memory Node)。各个空闲的内存节点通过指针形成一个链表,链表的每一个内存节点都是一块可供分配的内存空间; (3)某个内存节点一旦分配出去,从空闲内存节点链表中去除; (4)一旦释放了某个内存节点的空间,又将该节点重新加入空闲内存节点链表; (5)如果一个内存块的所有内存节点分配完毕,若程序继续申请新的对象空间,则会再次申请一个内存块来容纳新的对象。新申请的内存块会加入内存块链表中。
如上图所示,申请的内存块存放三个可供分配的空闲节点。空闲节点由空闲节点链表管理,如果分配出去,将其从空闲节点链表删除,如果释放,将其重新插入到链表的头部。如果内存块中的空闲节点不够用,则重新申请内存块,申请的内存块由内存块链表来管理。
注意,本文涉及到的内存块链表和空闲内存节点链表的插入,为了省去遍历链表查找尾节点,便于操作,新节点的插入均是插入到链表的头部,而非尾部。当然也可以插入到尾部,读者可自行实现。#include#include #include using namespace std;template class MemPool{private: //空闲节点结构体 struct FreeNode { FreeNode* pNext; char data[ObjectSize]; }; //内存块结构体 struct MemBlock { MemBlock* pNext; FreeNode data[NumofObjects]; }; FreeNode* freeNodeHeader; MemBlock* memBlockHeader;public: MemPool() { freeNodeHeader = NULL; memBlockHeader = NULL; } ~MemPool() { MemBlock* ptr; while (memBlockHeader) { ptr = memBlockHeader->pNext; delete memBlockHeader; memBlockHeader = ptr; } } void* malloc(); void free(void*);};//分配空闲的节点template void* MemPool ::malloc(){ //无空闲节点,申请新内存块 if (freeNodeHeader == NULL) { MemBlock* newBlock = new MemBlock; newBlock->pNext = NULL; freeNodeHeader = &newBlock->data[0]; //设置内存块的第一个节点为空闲节点链表的首节点 //将内存块的其它节点串起来 for (int i = 1; i < NumofObjects; ++i) { newBlock->data[i - 1].pNext = &newBlock->data[i]; } newBlock->data[NumofObjects - 1].pNext = NULL; //首次申请内存块 if (memBlockHeader == NULL) { memBlockHeader = newBlock; } else { //将新内存块加入到内存块链表头部 newBlock->pNext = memBlockHeader; memBlockHeader = newBlock; } } //返回空节点闲链表的第一个节点 void* freeNode = freeNodeHeader; freeNodeHeader = freeNodeHeader->pNext; return freeNode;}//释放已经分配的节点template void MemPool ::free(void* p){ FreeNode* pNode = (FreeNode*)p; pNode->pNext = freeNodeHeader; //将释放的节点插入空闲节点头部 freeNodeHeader = pNode;}class ActualClass{ static int count; int No;public: ActualClass() { No = count; count++; } void print() { cout << this << ": "; cout << "the " << No << "th object" << endl; } void* operator new(size_t size); void operator delete(void* p);};//定义内存池对象MemPool mp;void* ActualClass::operator new(size_t size){ return mp.malloc();}void ActualClass::operator delete(void* p){ mp.free(p);}int ActualClass::count = 0;void AchieveMemPool(){ ActualClass* p1 = new ActualClass; p1->print(); ActualClass* p2 = new ActualClass; p2->print(); delete p1; p1 = new ActualClass; p1->print(); ActualClass* p3 = new ActualClass; p3->print(); delete p1; delete p2; delete p3;}
阅读以上程序,应注意以下几点。
(1)对一种特定的类对象而言,内存池中内存块的大小是固定的,内存节点的大小也是固定的。内存块在申请之初就被划分为多个内存节点,每个Node的大小为ItemSize。刚开始,所有的内存节点都是空闲的,被串成链表。 (2)成员指针变量memBlockHeader是用来把所有申请的内存块连接成一个内存块链表,以便通过它可以释放所有申请的内存。freeNodeHeader变量则是把所有空闲内存节点串成一个链表。freeNodeHeader为空则表明没有可用的空闲内存节点,必须申请新的内存块。 (3)申请空间的过程如下。在空闲内存节点链表非空的情况下,malloc过程只是从链表中取下空闲内存节点链表的头一个节点,然后把链表头指针移动到下一个节点上去。否则,意味着需要一个新的内存块。这个过程需要申请新的内存块切割成多个内存节点,并把它们串起来,内存池技术的主要开销就在这里。 (4)释放对象的过程就是把被释放的内存节点重新插入到内存节点链表的开头。最后被释放的节点就是下一个即将被分配的节点。 (5)内存池技术申请/释放内存的速度很快,其内存分配过程多数情况下复杂度为O(1),主要开销在freeNodeHeader为空时需要生成新的内存块。内存节点释放过程复杂度为O(1)。 (6) 在上面的程序中,指针p1和p2连续两次申请空间,它们代表的地址之间的差值为8,正好为一个内存节点的大小sizeof(FreeNode))。指针p1所指向的对象被释放后,再次申请空间,得到的地址与刚刚释放的地址正好相同。指针p3多代表的地址与前两个对象的地址相聚很远,原因是第一个内存块中的空闲内存节点已经分配完了,p3指向的对象位于第二个内存块中。