简易内存池

Posted Moua

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了简易内存池相关的知识,希望对你有一定的参考价值。

目录

一、内存池基础知识

1、什么是内存池

1.1 池化技术

1.2 内存池

2、内存池的作用

2.1 效率问题

2.2 内存碎片

3、内存池技术的演进

二、简易内存池原理

1、整体设计

1.1 内存池结构

1.2 申请内存

1.3 释放内存

2、详细剖析

2.1 blockNode结构

2.2 单个对象的大小

3、性能比较

三、简易内存池完整源码


一、内存池基础知识

1、什么是内存池

1.1 池化技术

池化技术是计算机中的一种设计模式,主要是指:将程序中经常要使用的计算机资源预先申请出来,由程序自己管理,程序在使用时直接从“池”中获取,不仅保证了程序占有的资源数量同时减少资源的申请和释放时间。常见的池化技术有内存池、线程池、连接池等。

1.2 内存池

内存池是一种动态内存分配与管理技术。它的核心思想是:预先申请一段内存空间,使用一种高效的数据结构(哈希、链表)进行管理,当程序需要内存时直接从内存池中分配一块内存给程序,同样当使用完时在归还给内存池。这样做的好处是,减少直接使用new/delete、malloc/free等API申请和释放内存的时间,提高程序运行效率;同时,程序每次直接使用new/delete、malloc/free从内存中申请空间,会导致内存碎片问题,内存池直接申请大块内存就减少了内存碎片。

2、内存池的作用

2.1 效率问题

通常申请内存都是通过new/delete、malloc/free接口直接从内存的堆区申请一块内存,释放也是直接释放到堆中。频繁的申请和释放必然消耗大量时间,降低程序的运行效率。

例如:假设每个链表的节点大小为16字节,当链表需要经常插入节点时,必然就需要频繁的内存申请操纵,每次从堆中申请16个字节都要一定的时间开销,释放内存也需要时间开销。使用内存池,我们可以直接从内存中申请“一批节点”,当程序需要内存时不用直接去堆中申请,直接将预先申请好的内存分配给程序。

2.2 内存碎片

频繁的从内存中申请小块内存会导致内存碎片问题。内存碎片分为内碎片和外碎片两种。

1)外碎片

外碎片也就是我们常说的内存碎片。例如:我们每次从内存中申请一块16字节大小的内存,内存中就会存在很多16个字节大小的块,当该内存释放时就可能造成内存碎片,如下图:

内存中空闲内存大小为88字节,但是我们能申请的最大内存块为21字节。

2)内碎片

内碎片是指已经分配出去的内存中存在的未使用的小块内存。内存池技术虽然解决了内存随便但是又造成了内碎片问题,内碎片不可避免但是可以通过程序的优化减少内存内碎片。

例如:实际需要是申请10byte的内存,定长内存池可能会进行内存对齐,一次性分配了16个字节的内存,多余的6字节实际并未使用,这6字节就是内存内碎片。

3、内存池技术的演进

1)最最最最“简单”的内存池

做一个链表,指向空闲的内存。分配就是从链表中取出来一块返回pop,释放就是将内存在push到链表中。需要做好归并,标记和保护,防止内存二次释放问题。

2)定长内存池

实现一个FreeList类,它的本质是一个链表,节点是一块固定大小的内存,采用头插和头删的方式申请释放内存。每个固定内存分配器里面有两个链表:OpenList用于存储未分配的空闲内存对象(FreeList对象),CloseList用于存储已经分配的内存对象。

分配内存就是从IOpenLsit中取出一个对象给程序,释放内存就是将对象push到CloseList里。当内存不够时,OpenList申请一个大块内存在切割成固定的长度大小的小块内存。

3)C++STL库中的内存池

定长内存池存在的问题就是只能申请固定长度的内存,而实际中我们需要申请的内存大小可能是不管固定,在C++STL库中,采用哈希表和定长内存池结合的方式实现了一个内存池。具体如下

构造多个定长内存池,以一个固定的对齐数进行对齐(例如以8字节进行对齐),第一个定长内存池的内存对象大小为8(至少得能保证无论在64位还是32位系统下都可以保存下一个指针类型),第二个内存池对象大小为16...最后一个内存池对象大小为128byte,当申请的内存大小超过128字节时,通过二级空间配置器申请(直接从内存中申请)。

构造一个哈希表,将不同大小的内存对象挂在哈希表中。如下图:

申请内存:加入要申请的内存大小为8字节直接在index  = 0处分配一块内存,当然申请的内存小于8字节时也会直接分配8字节的内存。当Free_list[index]为nullptr时从内存中申请一块内存,切割成固定大小‘挂在’Free_list[index]位置。

释放内存:根据内存对象大小,计算index在插入到哈希表中的index位置。

二、简易内存池原理

1、整体设计

1.1 内存池结构

两个链表,RequestMemory和ReleaseMemory。

RequestMemory链表存储的是使用new或者malloc从物理内存申请的还没有被使用的内存块,是一个个的memNode节点。

ReleaseMemory链表存储的是使用完释放回来的固定大小的内存块。

1.2 申请内存

  • 先在ReleaseMemory找,如果有内存则直接pop使用
  • ReleaseMemory为nullptr时,在RequestMemory中找。
  • RequestMemory的头节点表示的是新申请的,申请内存时只需要在头结点中找,判断头结点的useCount和sumCount是否相等。当useCount等于sumCount时表示已经用完了,就需要去物理内存中申请,否则直接从表头push一块。
  • 去物理内存申请内存时,申请的大小是上一次申请内存块大小的二倍,并将申请的内存块push到RequestMemory头部。

1.3 释放内存

  • 释放内存时,直接将要释放的内存push到ReleaseMemory的头部即可。

2、详细剖析

2.1 blockNode结构

blockNode表示一个个新申请的内存块,用一个结构体进行管理。blockNode成员如下:

  • void* _memory:表示新申请的内存块的首地址
  • BlockNode * _next:存储next节点
  • _objNum:内存块对象的个数

注意:blockNode的大小每次都是上一次的二倍,是一个质数增长,因此应该设置一个上限,当到达一定大小后进行线性增长。这里规定,最大内存块的大小为100000*sizeof(T),T表示的是申请的节点类型。

2.2 单个对象的大小

这里的单个对象指的ReleaseMemory的节点大小,当用户申请的内存大小sizeof(T)小于sizeof(T*)时,为了能够将该对象链接到ReleaseMemory中,应该按照T*进行分配。

3、性能比较

分别使用malloc/free、new/delete、memPool申请和释放110000个内存,时间如下:

三、简易内存池完整源码

#include<iostream>
#include<vector>
#include<ctime>
using namespace std;

template<class T>
class MemPool
{
private:
	//内存块结构
	typedef struct BlockNode
	{
		void* _memory;//内存块地址
		BlockNode* _next;//下一个blockNode
		size_t _objNum;//内存块对象的个数
		//构造函数---num表示申请对象的个数
		BlockNode(size_t num)
			:_objNum(num),
			_next(nullptr)
		{
			_memory = malloc(_objNum*_size);
		}

		~BlockNode()
		{
			free(_memory);
			_memory = nullptr;
			_next = nullptr;
			_objNum = 0;
		}
	}BlockNode;
protected:
	static size_t _size;//单个对象的大小
	T* _releaseMemory = nullptr;//释放的内存
	BlockNode* _requestMemory;//申请的内存块
	size_t _maxNum;//内存块最大的大小
	size_t _useCount;//当前内存块已经使用的对象个数
protected:
	//设置单个对象的大小
	static size_t setSize()
	{
		return (sizeof(T) >= sizeof(T*) ? sizeof(T):sizeof(T*));
	}
public:
	MemPool()
		:_useCount(0),
		_releaseMemory(nullptr),
		_maxNum(100000*_size)
	{
		//开始先申请32个_size大小的空间
		_requestMemory = new BlockNode(32);
	}

	~MemPool()
	{
		BlockNode *cur = _requestMemory;
		while (cur)
		{
			BlockNode* del = cur;
			cur = cur->_next;
			delete del;            //会自动调用~BlockNode()
		}
	}

	T* New()
	{
		//先在releaseMemory中找
		if (_releaseMemory)
		{
			T* obj = _releaseMemory;
			_releaseMemory = *((T**)_releaseMemory);//releaseMemory的前几个字节存储的是下一个节点的地址
			return obj;
		}
		else
		{
			//判断requesetMemory中是否还有空闲内存
			if (_requestMemory->_objNum == _useCount)
			{
				//取物理内存中申请一块内存
				size_t size = 2 * _useCount >= _maxNum ? _maxNum : 2 * _useCount;
				BlockNode* newBlock = new BlockNode(size);

				newBlock->_next = _requestMemory;
				_requestMemory = newBlock;
				_useCount = 0;
			}
			//走到这里,一定有内存
			T* obj = (T*)((char*)_requestMemory->_memory+_useCount*_size);

			_useCount++;
			return new(obj)T();//用定位new对这块空间初始化
		}
	}

	void Delete(T* obj)
	{
		if (obj)
		{
			obj->~T();

			*((T**)obj) = _releaseMemory;
			_releaseMemory = obj;
		}
	}
};

//静态成员变量,类外初始化
template<typename T>
size_t MemPool<T>::_size = MemPool<T>::setSize();

struct TreeNode
{
	int _val;
	TreeNode* _left;
	TreeNode* _right;
};
void test1()
{
	MemPool<TreeNode> mp;

	vector<TreeNode*> v;
	for (int i = 0; i < 10; i++)
	{
		TreeNode* mem = mp.New();
		v.push_back(mem);
	}

	for (int i = 0; i < 10; i++)
	{
		mp.Delete(v[i]);
	}
}

以上是关于简易内存池的主要内容,如果未能解决你的问题,请参考以下文章

简易内存池

简易内存池

华为机试真题 C++ 实现简易内存池

独家提供华为OD机试 C 语言题解 - 简易内存池2

转内存管理内幕mallco及free函数实现--简易内存分配器内存池GC技术

14.VisualVM使用详解15.VisualVM堆查看器使用的内存不足19.class文件--文件结构--魔数20.文件结构--常量池21.文件结构访问标志(2个字节)22.类加载机制概(代码片段