一、为什么要使用内存池呢?
在了解内存池之前,我们先来了解一下什么是内存碎片:
内存碎片
通常情况下,我们在使用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();
以上就是一个简单的内存池的设计过程。