一、爲什麼要使用內存池呢?
在瞭解內存池之前,我們先來了解一下什麼是內存碎片:
內存碎片
通常情況下,我們在使用new、malloc進行空間申請時,系統都是在對上進行空間開闢的,儘管開闢出來內存的地址是連續的一塊內存空間,但每次開闢的內存塊的地址並不是連續的,這樣的話當我麼開闢的次數變多以後,堆上就剩餘許多小塊的空間導致在我們需要一塊比較大的空間時會開闢失敗。這是我們最常聽到的一種內存碎片,也成爲“外碎片”。
其實還有另外一種內存碎片,稱爲“內碎片”,內碎片是開闢出的一塊空間相對於需要的空間比較大一點,因此在已經開闢出的空間上有一部分未使用,內碎片帶來最明顯的問題就是空間浪費。
如圖所示:
爲了克服這種問題我們提出了內存池的概念。
二、內存池
內存池是一種內存分配技術,在使用內存之前,系統先分配一大塊內存當作備用,當我們需要一塊新的內存時,就去這個備用內存塊上去切分一部分來使用,這個備用內存也就是所謂的內存池,當這塊內存被切分完後我們再去向系統申請一塊更大的內存(一般是前一塊內存的2倍)。
當使用完申請的小塊內存後需要將內存塊還回去,這裏不會將內存還給操作系統,而是還回給內存池。我們可以在這個內存池上進行不斷申請空間、釋放空間的操作,由於不是去系統裏面申請、釋放小塊內存,所以進行反覆申請、釋放時內存池比系統的更高效,更重要的是,內存池的實現緩解了內存碎片的問題。
總結內存池主要有以下優點:
- 由於向內存申請的內存塊都是比較大的,所以能夠降低外碎片問題。
- 一次性向內存申請一塊大的內存慢慢使用,避免了頻繁的向內存請求內存操作,提高內存分配的效率。
三、模擬實現內存池
當我們向系統申請一塊空間的時候,我們必須想辦法把這塊空間管理起來 ,纔不會有太多空間浪費,內存泄漏的問題出現,所以我們可以使用鏈表結構管理起來,內存池的簡單模型如下圖所示:
從上圖,我們可以發現,雖然內存池在一定程度上比直接在系統上開闢空間更高效一點,但是在空間使用上還是會存在一些空間浪費問題,那麼怎麼才能使內存利用率達到最高呢,顯而易見,我們必須想辦法將釋放回來的空間進行重複利用,這樣就可以提高內存的利用率了。
所以呢這裏我們用一個_endFree指針來標誌最後一個釋放的內存地址空間,並且用該指針不斷保存前一塊釋放的空間的地址空間,如此就將所有的釋放的地址空間鏈接起來了,便於我們使用和管理
其具體過程如下:
鑑於以上思路,可以總結內存池的實現步驟如下:
(1)初始化內存池
在創建內存池的時候爲內存池分配了一塊很大的內存,便於以後的使用。
(2)分配內存
根據new/malloc的大小從內存池中取所需空間進行相應的內存分配
(3)釋放內存
當用戶使用結束後,將對應的內存歸還給內存池
(4)回收內存
將釋放的內存空間採用類似鏈表的形式連接起來,便於後續使用
代碼實現:
#pragma once
#include<iostream>
using namespace std;
#include<cstdlib>
template<class T>
class MyMemPool
{
struct ListNode
{
ListNode* _next;//指向下一個節點的指針
void* _pMemory;//指向內存塊的指針
size_t _num;//內存對象的個數
ListNode(size_t num)
:_next(NULL)
, _num(num)
{
_pMemory = malloc(_size*_num);
}
~ListNode()
{
free(_pMemory);
_pMemory = NULL;
_next = NULL;
_num = 0;
}
};
public:
MyMemPool(size_t initnum = 32, size_t maxNum = 10000)//默認最開始的內存塊有32個,一個內存塊最大有maxNum個對象
:use_count(0)
, _maxNum(maxNum)
, _endFree(NULL)
{
_head = _tail = new ListNode(initnum);//先開闢一個能夠存放initnum個對象的結點
}
~MyMemPool()
{
Destroy();
_head = _tail = NULL;
}
T*New()//分配內存
{
if (_endFree)//先到釋放回來的內存中找
{
T*object = _endFree;
_endFree = *((T**)_endFree);//將_endFree轉換成T**,*引用再取出來T*,也就是取出前T*類型大小的單元
return new(object) T();//再將這塊內存運用重定位new表達式初始化
}
//判斷是否還有已經分配的內存但沒有被使用
if (use_count >= _tail->_num)//表示此時鏈表中已經沒有能夠使用的內存了
{
size_t size = 2 * use_count;//按兩倍的方式增長
if (size > _maxNum)
{
size = _maxNum;
}
_tail->_next = new ListNode(size);//將新開闢的內存掛到鏈表新的結點下
_tail = tail->_next;
use_count = 0;
}
//有可使用的內存塊
T* object = (T*)((char*)_tail->_pMemory + use_count*_size);
use_count++;
return new(object)T();
}
void Destory()
{
ListNode *cur = _head;
while (cur)
{
ListNode* del = cur;
cur = cur->_next;
delete del; //會自動調用~ListNode()
}
_head = _tail = NULL;
}
void Delete(T* object) //釋放內存
{
if (object)
{
object->~T();
*((T**)object) = _endFree; //將_endFree裏面保存的地址存到指向空間的前T*大小的空間裏面
_endFree = object;
}
}
protected:
static size_t GetItemSize()
{
if (sizeof(T)>sizeof(T*))
{
return sizeof(T);
}
else
{
return sizeof(T*);
}
}
protected:
size_t use_count;//統計當前節點在用的個數
ListNode* _head;//指向鏈表節點的頭指針
ListNode* _tail;//指向鏈表節點的尾指針
size_t _maxNum;//申請的內存塊的最大大小
static size_t _size;//單個對象的大小
T* _endFree;//指向最後一個釋放對象
};
//靜態數據成員類外初始化
template<typename T>
size_t MyMemPool<T>::_size = MyMemPool<T>::GetItemSize();
以上就是一個簡單的內存池的設計過程。