1.引入内存池的意义
内存池(Memory Pool)是一种内存分配方式,又被称为固定大小区块规划(fixed-size-blocks allocation)。通常我们习惯直接使用new、malloc等API申请分配内存,但是这种方式非常容易产生内存碎片,早晚都会申请内存失败。并且在比较复杂的代码或者继承的屎山中,非常容易出现内存泄漏。
内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是能够使内存分配效率得到提升。
2.内存池的实现原理
我们通过一个内存池类来操作内存池,这个类中的所有成员方法都是操作内存池的函数。在我们设想的内存池中,我们以4k为基准定义,申请空间超过4k的我们额外使用malloc分配一个自定义块,并且使用一个管理自定义块的对象来保存它的地址,以便于在释放空间的时候我们能够找到他。申请空间不超过4k的我们就将我们内存池的标准块中的一部分划分给它来减少malloc的次数,若当前标准块的空间不够,则额外开辟一块4k的空间加入内存池并且给它分配空间。
内存池中的类定义如下所示:
//用来管理标准块的类
class Mem_node
{
public:
//初始化node结点
void init();
//将node结点置回初始状态
void node_clean();
//分配成功,引用计数+1,移动start指针
void node_malloc(size_t size);
//减少引用计数,若为0移动start指针
void node_free();
uchar * get_start();
uchar * get_end();
int get_quote();
//获取end - start的值,也就是当前块现在的可用长度
size_t get_len();
Mem_node *get_next();
//每次调用get方法failed计数器自增1
int get_failed();
void set_next(Mem_node* next);
private:
uchar *m_start; //标记当前标准块可以使用的起始地址
uchar *m_end; //标记当前标准块可以使用的末尾地址
Mem_node *m_next; //标准快链表的next指针
int m_quote; //当前标准块的引用计数器,用来判断当前标准块是否被使用
int m_failed; //当前标准块的错误计数器,用来移动内存池的current指针提高效率
};
//用来管理自定义块的类
class Mem_large
{
public:
//使large结点的成员变量重置为参数所示的值
void large_reset(int size,void * alloc);
//释放掉large结点指向的自定义块内存空间
void large_free();
void set_next(Mem_large * next);
Mem_large * get_next();
void * get_alloc();
int get_size();
private:
Mem_large *m_next; //自定义块链表的next指针
int m_size; //用来存放自定义块所占用的内存大小
void *m_alloc; //用来存放自定义块所在的起始地址
};
class Mem_pool
{
public:
Mem_pool() = delete;
//构造函数,创建内存池,并初始化一块自定义大小的空间
explicit Mem_pool(size_t size); //类型转化构造函数,自定义的大小仅允许是4k的整数倍
//析构函数,销毁内存池,遍历各个标准块与大块,把从堆区分配的内存释放
~Mem_pool();
//给用户提供的申请内存空间的接口
void *mp_new(size_t size);
//给用户提供的申请内存空间的接口,并全部初始化为0
void *mp_new_zero(size_t size);
//给用户提供的释放已申请的内存空间的接口
void mp_delete(void *p);
//给用户提供的清空内存池中数据的接口
void mp_reset_pool();
//给用户提供的显示当前内存池使用情况的接口
void mp_monitor(string msg);
private:
//向堆空间申请一块标准块空间
void *mp_new_block(size_t size);
//向堆空间申请一块长度大于标准块长度的自定义大块空间
void *mp_new_large(size_t size);
Mem_node *m_head; //标准块链表的头指针
Mem_node *m_current; //标准块链表当前可能使用的指针(局部性原理)
Mem_large *m_large; //大块链表的头指针
};
2.1 内存池实现的主要功能
在此,笔者将简述一下内存池类(Mem_pool)的成员函数及其实现的主要功能:
Mem_pool(size_t size) :构造函数,创建内存池,并初始化一块自定义大小的空间
~Mem_pool() :析构函数,销毁内存池,遍历各个标准块与大块,把从堆区分配的内存释放
mp_new(size_t size) :给用户提供的申请内存空间的接口
mp_new_zero(size_t size) :给用户提供的申请内存空间的接口,并全部初始化为0
mp_delete(void *p) :给用户提供的释放已申请的内存空间的接口
mp_reset_pool() :给用户提供的清空内存池中数据的接口
mp_monitor(string msg) :给用户提供的显示当前内存池使用情况的接口
mp_new_block(size_t size) : 向堆空间申请一块标准块空间
mp_new_large(size_t size) : 向堆空间申请一块长度大于标准块长度的自定义大块空间
2.2 内存池空间分配的实现原理
在管理内存池的类(Mem_pool)中,存在着两个链表,其中一个是以管理标准块的类为数据域的不带表头结点的单链表,另一个是以管理自定义块的类为数据域的不带表头的单链表。
一个管理标准快的对象(Mem_node)管理一个标准块,且该对象的起始地址为该标准块的起始地址。Mem_node类有一个起始指针,一个结束指针与一个next指针,起始指针指向的是该标准块能够分配内存的起始地址,结束指针指向该标准块的结束地址,next指针用来指向下一个Mem_node对象用来形成链表。
一个管理自定义块的对象(Mem_large)管理一个自定义块,每当我们创建一个自定义块的时候我们才创建一个Mem_large对象来管理它,并且该对象在内存池的标准块中申请空间(换句话说就是Mem_large对象的内存空间由内存池的标准快分配)。Mem_large类有一个指向自定义块的指针,用来以后释放空间,还有一个next指针用来形成链表。
每当我们需要申请一块空间的时候,我们首先需要判断这块空间的大小是否大于一个标准块的大小(4k),若没有超过我们就将在标准块中分配这段空间。
假设现在我们在标准块中分配,我们还需要判断我们当前的标准块剩余的可用空间能否容纳下我们需要分配的空间。若可以,我们就将在当前的标准块中划分出空间来实现空间分配,并且减少当前标准块可以分配的空间,如图1所示。若不可以,我们将开辟出一块新的标准块,并实现上述操作,如图2所示。
图1 标准块剩余空间足够的情况
图2 标准块剩余空间不足需要开辟新的标准块的情况
假设我们需要分配的空间超过了4k,我们就需要开辟出一个能够容纳该空间的自定义块,将自定义块的内存空间分配出去,接着我们还需要创建一个管理自定义块的对象,这个对象的一个成员指针保存这段被分配的空间的地址用来便于以后释放,如图3所示。
图3 创建自定义块的情况
随着标准块的创建,我们也将创建一个管理该标准块的对象(Mem_node),并将其挂载在管理内存池的对象(Mem_pool)的链表中,所以我们只需要找到内存池就可以找到所有在内存池中分配的标准块。
随着自定义块的创建,该管理该自定义块的对象将被挂载在管理内存池的对象(Mem_pool)的链表中,所以我们只需要找到内存池就可以找到所有在内存池中分配的自定义块。
2.3 内存池空间释放的实现原理
当我们想要释放一段从内存池中申请的内存空间时,我们需要判断这段空间是存在在标准块中还是自定义块中。
若存在于自定义块中,我们将遍历内存池对象中的管理自定义块对象的链表,找到对应的Mem_large对象后,我们只需要释放该对象中指向自定义块的指针就可以实现自定义块空间的释放了。
若存在于标准块中,我们也需要遍历内存池对象中管理标准块对象的链表,找到对应的Mem_node对象后,我们需要将该对象的引用计数(m_quote)减一(这样的话就可以当作该内存空间已经被释放掉了)。此处需要补充一下,在我们在标准块中申请空间成功时,我们将在增加管理该块的对象中的引用计数,所以该对象的引用计数就是我们在该标准块中申请的内存空间个数。当该块的引用计数为0的时候,我们就可以把这个标准块当作一个刚刚创建的标准块来给其他申请空间的指针来分配空间了。
提醒:内存池的释放仅进行内存空间的释放,为了避免错误操作内存导致程序错误的情况,请一定记得在释放完空间之后将指针置为nullptr。
3 对new与delete的重载
在C++中我们可以对new与delete进行重载实现对堆空间内存申请操作的接管。实现代码如下:
Mem_pool mp(4096);
void *operator new(size_t size)
{
return mp.mp_new(size);
}
void operator delete(void *p)
{
mp.mp_delete(p);
}
这样的话我们就可以几乎感觉不到任何区别地来使用内存池来申请与释放内存空间了。
需要另外提示的一点是我们在对new重载之后,依然可以在new后面对类类型进行初始化从而进入到类的构造函数中,例如:Test * p = new Test(10);
4. C++内存池对纯C内存池的增强
在C++内存池中,我们将内存池封装成了一个对象,通过这个对象来操作内存池,通过提供成员函数接口的操作隐藏了具体的实现代码,可以更加安全与简洁地操作内存池。
另外,将内存池封装成类还可以遵循RAII原则,我们在内存池对象的构造函数里面对内存池进行初始化,在内存池的析构函数里面销毁内存池并且将内存池从内存中申请的所有空间释放,避免了内存泄漏的情况发生。
5. C++内存池的实现代码
C++内存池的实现代码如下所示:
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <string>
#define PAGE_SIZE 4096
using uchar = unsigned char;
using namespace std;
//对齐规则
#define MP_ALIGNMENT 16
#define mp_align(n, alignment) (((n) + (alignment - 1)) & ~(alignment - 1))
#define mp_align_ptr(p, alignment) (void *)((((size_t)p) + (alignment - 1)) & ~(alignment - 1))
class Mem_node
{
public:
//初始化node结点
void init();
//将node结点置回初始状态
void node_clean();
//分配成功,引用计数+1,移动start指针
void node_malloc(size_t size);
//减少引用计数,若为0移动start指针
void node_free();
uchar * get_start();
uchar * get_end();
int get_quote();
//获取end - start的值,也就是当前块现在的可用长度
size_t get_len();
Mem_node *get_next();
//每次调用get方法failed计数器自增1
int get_failed();
void set_next(Mem_node* next);
private:
uchar *m_start; //标记当前标准块可以使用的起始地址
uchar *m_end; //标记当前标准块可以使用的末尾地址
Mem_node *m_next; //标准快链表的next指针
int m_quote; //当前标准块的引用计数器,用来判断当前标准块是否被使用
int m_failed; //当前标准块的错误计数器,用来移动内存池的current指针提高效率
};
class Mem_large
{
public:
//使large结点的成员变量重置为参数所示的值
void large_reset(int size,void * alloc);
//释放掉large结点指向的自定义块内存空间
void large_free();
void set_next(Mem_large * next);
Mem_large * get_next();
void * get_alloc();
int get_size();
private:
Mem_large *m_next; //自定义块链表的next指针
int m_size; //用来存放自定义块所占用的内存大小
void *m_alloc; //用来存放自定义块所在的起始地址
};
class Mem_pool
{
public:
Mem_pool() = delete;
//构造函数,创建内存池,并初始化一块自定义大小的空间
explicit Mem_pool(size_t size); //类型转化构造函数,自定义的大小仅允许是4k的整数倍
//析构函数,销毁内存池,遍历各个标准块与大块,把从堆区分配的内存释放
~Mem_pool();
//给用户提供的申请内存空间的接口
void *mp_new(size_t size);
//给用户提供的申请内存空间的接口,并全部初始化为0
void *mp_new_zero(size_t size);
//给用户提供的释放已申请的内存空间的接口
void mp_delete(void *p);
//给用户提供的清空内存池中数据的接口
void mp_reset_pool();
//给用户提供的显示当前内存池使用情况的接口
void mp_monitor(string msg);
private:
//向堆空间申请一块标准块空间
void *mp_new_block(size_t size);
//向堆空间申请一块长度大于标准块长度的自定义大块空间
void *mp_new_large(size_t size);
Mem_node *m_head; //标准块链表的头指针
Mem_node *m_current; //标准块链表当前可能使用的指针(局部性原理)
Mem_large *m_large; //大块链表的头指针
};
Mem_pool mp(4096);
void *operator new(size_t size)
{
return mp.mp_new(size);
}
void operator delete(void *p)
{
mp.mp_delete(p);
}
void Mem_node::init()
{
m_start = (uchar *)this + sizeof(Mem_node);
m_end = (uchar *)this + PAGE_SIZE;
m_next = nullptr;
m_failed = 0;
m_quote = 0;
}
//将node结点置回初始状态
void Mem_node::node_clean()
{
m_start = (uchar *)this + sizeof(Mem_node);
m_failed = 0;
m_quote = 0;
}
//分配成功,引用计数+1,移动start指针
void Mem_node::node_malloc(size_t size)
{
m_quote++;
m_start += size;
}
//减少引用计数,若为0移动start指针
void Mem_node::node_free()
{
m_quote--;
}
uchar *Mem_node::get_start()
{
return m_start;
}
uchar *Mem_node::get_end()
{
return m_end;
}
int Mem_node::get_quote()
{
return m_quote;
}
//获取end - start的值,也就是当前块现在的可用长度
size_t Mem_node::get_len()
{
return (m_end - m_start);
}
Mem_node *Mem_node::get_next()
{
return m_next;
}
//每次调用get方法failed计数器自增1
int Mem_node::get_failed()
{
m_failed++;
return m_failed;
}
void Mem_node::set_next(Mem_node *next)
{
m_next = next;
}
void Mem_large::large_reset(int size, void *alloc)
{
m_size = size;
m_alloc = alloc;
}
void Mem_large::large_free()
{
free(m_alloc);
m_size = 0;
m_alloc = nullptr;
}
void Mem_large::set_next(Mem_large *next)
{
m_next = next;
}
Mem_large *Mem_large::get_next()
{
return m_next;
}
void *Mem_large::get_alloc()
{
return m_alloc;
}
int Mem_large::get_size()
{
return m_size;
}
//构造函数,创建内存池,并初始化一块自定义大小的空间
Mem_pool::Mem_pool(size_t size)
{
if (size < PAGE_SIZE || size % PAGE_SIZE != 0)
{
size = PAGE_SIZE;
}
//posix_memalign((void **)&pool, MP_ALIGNMENT, size);
//m_current = (Mem_node *)malloc(size);
posix_memalign((void **)&m_current, MP_ALIGNMENT, size);
m_head = m_current;
m_head->init();
m_large = nullptr;
}
//析构函数,销毁内存池,遍历各个标准块与大块,把从堆区分配的内存释放
Mem_pool::~Mem_pool()
{
Mem_large *large = m_large;
Mem_node *cur, *next;
for (large; large != nullptr; large = large->get_next())
{
if (large->get_alloc() != nullptr)
{
free(large->get_alloc());
}
}
cur = m_head;
while (cur != nullptr)
{
next = cur->get_next();
free(cur);
cur = next;
}
}
//给用户提供的申请内存空间的接口
void *Mem_pool::mp_new(size_t size)
{
if (size <= 0)
{
return nullptr;
}
else if (size >= PAGE_SIZE - sizeof(Mem_node))
{
return mp_new_large(size);
}
else
{
uchar *addr = nullptr;
Mem_node *cur = m_current;
while (cur != nullptr)
{
if (cur->get_len() >= size)
{
addr = cur->get_start();
cur->node_malloc(size);
return addr;
}
else
{
cur = cur->get_next();
}
}
return mp_new_block(size);
}
}
//给用户提供的申请内存空间的接口,并全部初始化为0
void *Mem_pool::mp_new_zero(size_t size)
{
void *mem_addr = mp_new(size);
if (mem_addr != nullptr)
{
memset(mem_addr, 0, size);
}
return mem_addr;
}
//给用户提供的释放已申请的内存空间的接口
void Mem_pool::mp_delete(void *p)
{
Mem_large *large = m_large;
Mem_node *node = nullptr;
//若需要删除的是自定义块
for (large; large != nullptr; large = large->get_next())
{
if (p == large->get_alloc())
{
large->large_free();
return;
}
}
//若要删除的是标准块
node = m_head;
for (node; node != nullptr; node = node->get_next())
{
if (((uchar *)node <= (uchar *)p) && ((uchar *)p <= (uchar *)node->get_end()))
{
node->node_free();
if (node->get_quote() == 0)
{
m_current = m_head;
}
return;
}
}
}
//给用户提供的清空内存池中数据的接口
void Mem_pool::mp_reset_pool()
{
Mem_node *node = nullptr;
Mem_large *large = nullptr;
for (large = m_large; large != nullptr; large = large->get_next())
{
if (large->get_alloc() != nullptr)
{
free(large->get_alloc());
}
}
for (node = m_head; node != nullptr; node = node->get_next())
{
node->node_clean();
}
m_current = m_head;
}
//给用户提供的显示当前内存池使用情况的接口
void Mem_pool::mp_monitor(string msg)
{
Mem_node *head = nullptr;
Mem_large *large = nullptr;
int i = 0;
printf("\r\n\r\n------start monitor poll------%s\r\n\r\n", msg.c_str());
for (head = m_head; head != nullptr; head = head->get_next())
{
i++;
if (m_current == head)
{
cout << "current==>第" << i << "块\n";
}
printf("第%02d块small block 已使用:%4ld 剩余空间:%4ld 引用:%4d failed:%4d\n", i,
head->get_start() - (uchar *)head,
head->get_len(), head->get_quote(), head->get_failed());
}
i = 0;
for (large = m_large; large != nullptr; large = large->get_next())
{
i++;
if (large->get_alloc() != nullptr)
{
cout << "第" << i << "块large block size=" << large->get_size() << "\n";
}
}
printf("\r\n\r\n------stop monitor poll------\r\n\r\n");
}
//向堆空间申请一块标准块空间
void *Mem_pool::mp_new_block(size_t size)
{
int ret;
uchar *block = nullptr;
uchar *addr = nullptr;
Mem_node *node = nullptr;
Mem_node *current = m_current;
Mem_node *cur = nullptr;
//int ret = posix_memalign((void **)&block, MP_ALIGNMENT, PAGE_SIZE);
//block = (uchar *)malloc(PAGE_SIZE);
ret = posix_memalign((void **)&block, MP_ALIGNMENT, PAGE_SIZE);
if (ret != 0)
{
cout << "malloc error" << endl;
return nullptr;
}
node = (Mem_node *)block;
node->init();
addr = (uchar *)mp_align_ptr(block + sizeof(Mem_node), MP_ALIGNMENT);
node->node_malloc(size);
for (cur = current; cur->get_next() != nullptr; cur = cur->get_next())
{
if (cur->get_failed() > 4)
{
current = cur;
}
}
cur->set_next(node);
m_current = current;
return addr;
}
//向堆空间申请一块长度大于标准块长度的自定义大块空间
void *Mem_pool::mp_new_large(size_t size)
{
uchar *big_addr = nullptr;
Mem_large *large = nullptr;
int ret;
int n = 0;
ret = posix_memalign((void **)&big_addr, MP_ALIGNMENT, size);
if (ret)
{
return nullptr;
}
large = m_large;
for (large; large != nullptr; large = large->get_next())
{
if (large->get_alloc() == nullptr)
{
large->large_reset(size, big_addr);
return big_addr;
}
if (n++ > 3) //防止过多次的遍历,限制次数
{
break;
}
}
large = (Mem_large *)mp_new(sizeof(Mem_large));
if (large == nullptr)
{
free(big_addr);
return nullptr;
}
large->large_reset(size, big_addr);
//头插法
large->set_next(m_large);
m_large = large;
return big_addr;
}
测试代码如下:
class Test
{
public:
Test() = delete;
Test(int a, double b, string c);
private:
int m_a;
double m_b;
string m_c;
};
Test::Test(int a, double b, string c) : m_a(a), m_b(b), m_c(c)
{
cout << "Test构造!\n";
}
int main()
{
char * p = nullptr;
string s1 = "show";
mp.mp_monitor(s1);
for(int i =0 ;i <100;i++)
{
p = (char *)mp.mp_new(50);
}
mp.mp_monitor(s1);
for(int i =0 ;i <20;i++)
{
p = (char *)mp.mp_new(60);
}
mp.mp_monitor(s1);
p = (char *)mp.mp_new(5000);
mp.mp_monitor(s1);
mp.mp_delete(p);
mp.mp_monitor(s1);
Test * p1 = new Test(1,2.0,s1);
mp.mp_monitor(s1);
return 0;
}
测试结果如下;
参考博客:带你用纯C实现一个内存池(图文结合) (qq.com)