1.内存池的引入
(1)内存池顾名思义就是存放内存的池子,直接分配一大块内存存起来,然后慢慢地切出来使用,用完了再还回到池子,后续还能再利用。
(2)如果我们需要申请一块空间,我们一般都会动态开辟一块空间(new或malloc),它们两个的共同点就是在堆上去申请空间,当然也是有一定限制的。如果我们频繁的去申请释放空间,就会造成许多的内存碎片,内存的浪费也就产生了。如果我们可以将自己需要大小的空间保存起来(不交给操作系统管理),下次需要的时候,从自己保存的内存池子里去取,这样不仅效率高,而且还不会造成太大的浪费。
(3)内存池的设计主要是为了应对一些特殊场景,比如说:操作系统频繁的分配和释放小块内存,不仅影响分配效率而且易造成外碎片问题,在某种程度上内存池就能很好地解决这类问题,并且内存池还可以避免内存泄露等问题,既然内存池有这么多的优点,那我们该如何去设计一个内存池呢?
源自韦易笑作者的一篇博文中,他在实现一个固定大小的内存分配器的过程中是这样思考的:
即实现一个 FreeList,每个 FreeList 用于分配固定大小的内存块,比如用于分配 32字节对象的固定内存分配器,之类的。每个固定内存分配器里面有两个链表,OpenList 用于存储未分配的空闲对象,CloseList用于存储已分配的内存对象,那么所谓的分配就是从 OpenList 中取出一个对象放到 CloseList 里并且返回给用户,释放又是从 CloseList 移回到 OpenList。分配时如果不够,那么就需要增长 OpenList:申请一个大一点的内存块,切割成比如 64 个相同大小的对象添加到 OpenList中。这个固定内存分配器回收的时候,统一把先前向系统申请的内存块全部还给系统。
(3)有关内碎片和外碎片的理解
造成堆利用率很低的一个主要原因就是内存碎片化。如果有未使用的存储器,但是这块存储器不能用来满足分配的请求,这时候就会产生内存碎片化问题。内存碎片化分为内部碎片和外部碎片。
1>内碎片:
内部碎片是指一个已分配的块比有效载荷大时发生的,内部碎片的大小就是已经分配的块的大小和它们的有效载荷之差。
2>外碎片:
外部碎片就是操作系统频繁分配和释放小块内存,导致有足够的内存但不能再连续分配出系统请求的空间大小。如图:
2.内存池的实现
1>这个内存池的思路很简单:每次分配一个node,一个node相当于一个小的对象池,这个小池子用完了,再分配一个尺寸大一倍的node,这些node是以链表的方式链接起来的。(每一个节点管理一块内存,设定各个内存块存储对象的多少按2倍增长)
如图内存池实现机制:
2>内存是如何分配的?
这里分三种情况:
@优先使用以前释放的空间;
@如果没有先前释放的空间了,且当前节点还剩下未使用的内存就去内存块中申请;
@当前节点下的内存不足以我们用来分配,就开辟新的节点以获得新节点下维护的内存对象个数.
情况一如下图:
上面还有个小问题要注意:
情况二如图:
情况三:
这种情况就是需要开辟新的节点以获得新节点下维护的内存对象个数,需要注意两点就是,新开辟出来的节点在用计数要置为0;链表尾指针last要变动(刚开始一个节点的时候_first和_last指向相同),_last=_last->_next;
3.下面是代码实现:
#pragma once
#include<vector>
#include<string>
#include<cstdlib>
#include<iostream>
using namespace std;
//用链表实现内存池,每个节点下面挂一块内存
template<class T>
class ObjPool
{
struct BlockNode
{
void* _memory; //内存块
BlockNode* _next; //指向下一个节点的指针
size_t _ObjNum; //内存对象的个数
BlockNode(size_t ObjNum)
:_ObjNum(ObjNum)
,_next(NULL)
{
_memory=malloc(_ItemSize*_ObjNum); //单个对象的大小与内存对象的个数的乘积
if(_memory==NULL)
{
cout<<"out of memory"<<endl;
exit(1);
}
}
~BlockNode()
{
free(_memory); //_memory是通过malloc在堆上开辟的
_memory=NULL;
_next=NULL;
_ObjNum=0;
}
};
public:
//固定大小的内存池
ObjPool(size_t InitNum=32,size_t MaxNum=100000)//一个节点下面先默认开32字节大小的内存
:_CountIn(0)
,_MaxNum(MaxNum)
,_LastDelete(NULL)
{
_first=_last=new BlockNode(InitNum);
}
~ObjPool()
{
//释放节点下面挂的内存块
BlockNode* cur=_first;
while(cur)
{
BlockNode* del=cur;
cur=cur->_next;
delete del; //调用节点的析构函数
del=NULL;
}
_first=_last=NULL;
_CountIn=0;
}
//申请空间
T* New()
{
//1.优先使用以前释放的空间
//2.去内存块中申请
//3.申请新的节点对象
if(_LastDelete)
{
T* obj=_LastDelete;
//强转为二级指针再解引用就能正确的取到该指针所指向的内容,解决了32位程序和64为程序的限制
_LastDelete=*((T**)_LastDelete);
//使用的是还回来的内存,所以没必要再对当前节点在用计数_CountIn++
return new(obj)T; //new定位表达式
}
//所有结点的内存块都没有可以使用的,则重新开辟新的节点
if(_CountIn==_last->_ObjNum)
{
size_t NewNodeObjNum=_last->_ObjNum * 2;
if(NewNodeObjNum>=_MaxNum)
_MaxNum=NewNodeObjNum;
_last->_next=new BlockNode(NewNodeObjNum);
if(_last->_next==NULL)
{
throw(-1);
}
_CountIn=0; //新开的节点,当前节点在用计数置为0
_last=_last->_next;
}
//在还没用的内存块下面去分配使用
T* obj = (T*)((char*)_last->_memory + _CountIn*_ItemSize);
_CountIn++;
return new(obj)T;
}
//使用完小块内存后还给内存池时,Delete先调对象的析构函数,然后将这块内存头插入自由链表中
void Delete(T* ptr)
{
if(ptr)
{
ptr->~T();
}
*(T**)ptr=_LastDelete;
_LastDelete=ptr;
}
static size_t Get_ItemSize()
{
//BlockNode中存储了void* 的一个指针,所以最低限度要开出一个能存放void*指针大小的对象空间
if(sizeof(void*) >= sizeof(T))
return sizeof(void*);
else
return sizeof(T);
}
protected:
size_t _CountIn; //当前节点在用的计数
BlockNode* _first; //指向链表的头
BlockNode* _last; //指向链表的尾
size_t _MaxNum; //节点申请内存块的对象个数
static size_t _ItemSize; //单个对象的大小
T* _LastDelete; //指向最后释放的空间
};
template<class T>
size_t ObjPool<T>::_ItemSize=ObjPool<T>::Get_ItemSize(); //类外调用静态成员函数
————————————————
版权声明:本文为优快云博主「lc_29503203」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/qq_29503203/article/details/53488832