【內存池的引入】
一般的,我們要動態申請內存時,都會調用malloc或new,由於需要申請的內存大小可能預先不知道,所以有時文門會頻繁的調用malloc或new,這就大大降低了程序的運行速率,同時還有可能引進內碎片問題。基於上述原因,引入了內存池的概念。
【什麼是內存池】
內存池是一種內存分配方式。通俗一點說,內存池就是存放內存的池子。它可以不有效的減少內存碎片化、分配內存時速度更快、並且可以減少內存泄漏等。
通常,內存池會在申請內存前,先申請一個大塊的內存以供備用,當真正需要內存時,就去內存池中爲其分配一定大小的內存,當分配出去的內存用完之後,不是直接還給系統,而是直接還給爲其分配內存的內存池。
【運用內存池機制的好處】
1、由於內存池想系統申請的內存都是比較大塊的內存,所以能夠降低外碎片問題。
2、內存池一次性分配的內存可以夠很多次使用,避免了頻繁的向系統申請內存的操作,有效地提高了內存分配效率。
【內存碎片化】
造成堆利用率很低的一個主要原因就是內存碎片化。如果有未使用的存儲器,但是這塊存儲器不能用來滿足分配的請求,這時候就會產生內存碎片化問題。內存碎片化分爲內部碎片和外部碎片。
內碎片:
內部碎片是指一個已分配的塊比有效載荷大時發生的。(舉個栗子:假設以前分配了10個大小的字節,現在只用了5個字節,則剩下的5個字節就會內碎片)。內部碎片的大小就是已經分配的塊的大小和他們的有效載荷之差的和。因此內部碎片取決於以前請求內存的模式和分配器實現的模式。
外碎片:
外部碎片就是當空閒的存儲器的和計起來足夠滿足一個分配請求,但是沒有一個單獨的空閒塊足夠大可以處理這個請求。外部碎片取決於以前的請求內存的模式和分配器的實現模式,還取決於於將來的內存請求模式。所以外部碎片難以量化。
【設計固定大小的內存池】
每個對象的大小固定,每次想系統申請n個對象大小的內存塊,如果這n個對象全部被使用,就向系統再申請n個對象大小的內存(n *= 2),用鏈表的方式掛到上一次申請內存塊的後面,使用完的對象快用一個指針維護,以供重複利用。每次申請對象時,優先去最近釋放的對象指針中去找,如果沒有釋放的對象塊,纔去內存池中去申請。回收內存時,依次將申請的大塊內存釋放掉即可。
【代碼實現】
#pragma once
//設計簡易內存池,即固定大小的內存池,也稱作對象池
template <class T>
class ObjectPool
{
//定義一個內部類,用來保存每個大塊內存的節點
struct Node
{
Node* _memory; //指向大塊內存的指針
size_t _n; //當前節點裏面的對象個數
Node* _next; //指向下一個大塊內存節點
Node(size_t nobjs) //構造函數
{
_n = nobjs;
_memory = (Node*)::operator new(_n * GetObjSize());//調用operator new失敗後直接拋異常,不會調構造函數
_next = NULL;
}
~Node() //析構函數
{
::operator delete(_memory);//operator delete直接析構對象,不會調析構函數
_memory = NULL;
_next = NULL;
_n = 0;
}
};
public:
ObjectPool(size_t initNobjs = 16,size_t maxNobjs = 1024)//構造函數
:_initNobjs(initNobjs)
,_maxNobjs(maxNobjs)
{
_head = _tail = new Node(initNobjs); //默認初始大塊內存節點創建16個對象
_useInCount = 0; //剛開始還沒有使用對象
_lastDelete = NULL; //也沒有釋放回來的節點
}
~ObjectPool() //析構函數
{
Node* cur = _head;
while (cur)
{
Node* next = cur->_next; //保存下一個大塊內存節點
delete cur; //釋放當前節點
cur = next; //更新當前節點
}
_head = _tail = NULL;
}
inline static size_t GetObjSize() /*獲取每個對象的大小,\
\設置靜態成員函數,保證在類中的可見性\
\設置內聯函數,提高運行速率*/
{
return sizeof(T)>sizeof(T*) ? sizeof(T):sizeof(T*);
}
//O(1)
void* Allocate() //申請空間
{
//優先申請釋放回來的對象
if (_lastDelete)//如果_lastDelete不爲空,說明有釋放回來的對象,應重複利用
{
void* obj = _lastDelete; //用obj保存最近一次釋放的對象
_lastDelete = *(T**)_lastDelete;/*隱式鏈表:將_lastDelete強轉爲T**,然後\
\解引用取到前sizeof(T*)個字節,即第二個\
\最近釋放的對象,然後將其賦值給_lastDelete*/
return obj; //返回obj
}
//到Node裏面獲取對象,如果沒有內存可使用,就分配更大塊的內存
if (_useInCount >= _tail->_n)
{
AllocateNewNode();//分配更大塊的內存,有消耗
}
//內存池中還有已經分配好但未使用的內存
void* obj = (char*)_tail->_memory + _useInCount*GetObjSize();//將指針偏移到未使用的對象處
_useInCount++; //已經使用的對象個數++
return obj;
}
void Deallocate(void* ptr) //釋放內存
{
if (ptr)
{
//相當於頭插
*(T**)ptr = _lastDelete;//將最近釋放對象的隱式鏈表的地址賦給當前要釋放對象的前(T*)個字節
_lastDelete = (T*)ptr;//將當前要釋放的對象的地址賦值給_lastDelete
}
}
//模板函數
template <class Val>
T* New(const Val& val)//申請對象並且初始化
{
void* obj = Allocate();
return new(obj)T(val);//調用new定位表達式對新節點進行初始化
}
void Delete(T* ptr)//釋放內存並清理對象
{
if (ptr)
{
ptr->~T(); //顯示調用析構函數來清理對象
Deallocate(ptr);
}
}
protected:
void AllocateNewNode() //申請大塊內存節點
{
size_t n = _tail->_n * 2;//每次申請對象個數爲上個節點的2倍
if (n > _maxNobjs) //最大節點的上限爲_maxNobjs個對象
n = _maxNobjs;
Node* node = new Node(n);
_tail->_next = node; //將申請的新結點鏈到鏈表末端
_tail = node; //更新尾節點
_useInCount = 0; //將新結點的已使用的對象更新爲0
}
protected:
size_t _initNobjs; //想要申請的大塊內存的對象個數(默認值爲16)
size_t _maxNobjs; //大塊內存節點最多可分配的對象個數(默認最大爲1024)
Node* _head; //指向大塊內存鏈表的頭
Node* _tail; //指向大塊內存鏈表的尾
size_t _useInCount; //當前節點已經用了多少個對象
T* _lastDelete; //指向最新釋放的對象空間
};
void TestObjectPool()
{
ObjectPool<string> pool; //定義一個內存池對象
string* p1 = (string*)pool.Allocate();//使用其中的一個對象
string* p2 = (string*)pool.Allocate();//再使用其中的一個對象
pool.Deallocate(p1); //釋放對象
pool.Deallocate(p2); //釋放對象
string* p3 = (string*)pool.Allocate();//理應重複使用釋放的對象
string* p4 = (string*)pool.Allocate();//理應重複使用釋放的對象
ObjectPool<string> pool1;
string* p5 = pool1.New("測試");
pool.Delete(p5);
string* p6 = (string*)pool1.Allocate();
pool1.Deallocate(p6);
pool.Deallocate(p3);
pool.Deallocate(p4);
}