项目中经常需要用到new申请一些动态内存保存动态生成的数据,内存不大,基本都是几个到几十个byte的数据,但是这些小内存如何释放却成了问题:因为在下一次重绘之前这些动态数据是都不能删除的,而且new所在的位置又分布在不同的函数中,重绘之前回收内存根本无法确定需要回收哪些new对象以及多大的内存。
如果每次new时,是直接从一个我们进程自己管理的大内存块中分配一小块给当前程序使用,程序使用完毕也无需释放,只需要在结束时将管理的整块大内存一起释放返还给系统即可。
基于以上想法,在项目中添加了一个简单的内存管理类:classPEMemory。
- l 在程序初始时,PEMemory向系统new申请一大片内存(如1MB大小)
- l 在绘制过程中,如果需要new,则通过PEMemory分配所需的内存大小,绘制完成无需释放这些内存
- l 程序结束时,PEMemory直接返还给系统初始时申请的一大片内存
1、 考虑到不同类型,PEMemory使用了一个模板函数:
//! 使用【placement new】,在申请的内存上构造T类型对象
template <class T>
inline T* New(const T &value)
{
if ((_ptr_New + sizeof(T)) >= _ptr_ChunkEnd) {
alloc_memory(); // 内存不足,需要先向系统申请一片新内存
}
//【placement new】在_ptr_ChunkBegin上调用T的构造函数
// 注意: 使用placement new,需要客户端显式调用T的析构函数(我们这里就不需要了)
::new((T*)_ptr_New) T(value);
_ptr_New += sizeof(T); // 更新当前标志指针到新的可用内存
return (T*)(_ptr_New - sizeof(T));
}
New()函数采用一个T类型的引用对象作为参数,在我们的内存片上(_ptr_New指针)通过placement new直接构造T类型对象,并返回新构造的T类型对象指针。
2、 为了方便使用,同时提供了另外一个函数:
//! 在申请的内存上获取大小为size字节的空间
void* New2(int size)
{
if ((_ptr_New + size) >= _ptr_ChunkEnd) {
alloc_memory(); // 内存不足,需要先向系统申请一片新内存
}
_ptr_New += size; // 更新当前标志指针到新的可用内存
return (void*)(_ptr_New - size);
}
返回指针后,可以继续使用::new((T*)_ptr_New)T(value); 以构造所需的对象。
完整的代码附在最后。
说明:
这里只是简单的管理动态内存,不过对于项目来说也够了。这种方式能够减少内存碎片,方便快捷的回收内存,有效避免项目中的内存泄露问题。
想法其实来源于《STL源码剖析》这本书的第二章,其中详细分析了STL的内存分配器的行为。
PEMemory最大的缺点就是:使用过的内存不能被后面的继续使用。好在项目中用到的内存都不大,一次重绘需要的总内存也不会超过几kb。
改进:(详见《STL源码剖析》)
将PEMemory申请的一大块内存片等分成若干份小内存块,每块为固定字节大小,再将这些内存块组织成一个链表。需要内存时,直接查找这个链表,将可用的一自由块分配给程序使用,并将这块内存块从链表记录中删除。内存块使用完毕时,不是直接返回给系统,而是将其添加到链表中,供下次申请时分配使用。示意图如下:
这样就可以改善PEMemory中“使用过的内存不能被后面的继续使用”这一致命缺点。不过,问题依旧存在,链表中的每个内存块大小是固定的N,如果需要的内存比N小,就会有浪费,如果需要的内存比N大,就会不能满足需求了。
进一步改进:
为了实现程序中需要多大内存,就分配多大内存,我们对上面的链表进一步改进。
我们将内存需求规划为4B、8B、16B、32B、。。。512B、1024B、等等这些组,每个组分配若干个对应大小的内存块,每一组的内存块连接成一个子链表,所有这些子链表再连接成总的链表free_list。程序中需要内存时,先判断大小,将其扩大成最接近的一个组的大小,再在这个组中查找一个自由块分配给程序使用,程序使用完毕再将这块内存返回给这个组的链表中。
这也是STL内存分配的思想。
附录:
#pragma once
#include <vector>
namespace base {
/**
* @class PEMemory pememory.h
* @brief PaintEngine类中的new内存管理类
* @author FCQ
* @date February 10,2014
* @note
* 作为PaintEngine类的一个基础,提供 【placement new】
* 预先向 system 申请一大块内存,在需要new的地方提供已经申请好的内存
* 减小内存碎片、快捷、回收方便,避免内存泄露
* 【注意】简单的模型,通过标志指针_ptr_New不断分配空内存,使用期间内存不会回收,直到PEMemory对象被析构(即标志指针是单调递增的)
*/
class PEMemory
{
public:
PEMemory();
//! 析构函数,在这里释放申请得到的内存
~PEMemory();
//! 使用【placement new】,在申请的内存上构造T类型对象
template <class T>
inline T* New(const T &value)
{
if ((_ptr_New + sizeof(T)) >= _ptr_ChunkEnd) {
alloc_memory(); // 内存不足,需要先向系统申请一片新内存
}
//【placement new】在_ptr_ChunkBegin上调用T的构造函数
// 注意: 使用placement new,需要客户端显式调用T的析构函数(我们这里就不需要了)
::new((T*)_ptr_New) T(value);
_ptr_New += sizeof(T); // 更新当前标志指针到新的可用内存
return (T*)(_ptr_New - sizeof(T));
}
//! 在申请的内存上获取大小为size字节的空间
void* New2(int size)
{
if ((_ptr_New + size) >= _ptr_ChunkEnd) {
alloc_memory(); // 内存不足,需要先向系统申请一片新内存
}
_ptr_New += size; // 更新当前标志指针到新的可用内存
return (void*)(_ptr_New - size);
}
private:
//! 向系统申请一大片内存
char *alloc_memory();
private:
char *_ptr_ChunkBegin; ///< 预先申请分配的大片内存的地址
char *_ptr_ChunkEnd; ///< 预先申请分配的大片内存的结尾的下一地址(非法地址,仅供判断结尾之用)
char *_ptr_New; ///< 每次调用模板New时,新构造对象的地址(即尚未使用的内存起始处)
unsigned int _MemorySize; ///< 当前已分配的内存的总大小
std::vector<char *> _vec_ChunkRecord; ///< 记录每个分配的内存片的首地址,以便最后的回收
};
} // namespace base
// PEMEMORY_H
#include "pememory.h"
#include <QMessageBox>
using namespace std;
using namespace base;
#define DEFAULT_CHUNK_SIZE 1*1024*1024 ///< 默认申请的供使用的内存片的大小为 1 MB
PEMemory::PEMemory() :_ptr_ChunkBegin(0),_ptr_ChunkEnd(0), _MemorySize(0), _ptr_New(0)
{
}
//! 析构函数,在这里释放申请得到的内存
PEMemory::~PEMemory()
{
char *ptr;
for (int i=0; i<_vec_ChunkRecord.size(); i++)
{
ptr = _vec_ChunkRecord.at(i);
delete[] ptr; // 释放内存片
}
}
//! 向系统申请一大片内存
char *PEMemory::alloc_memory()
{
unsigned int chunk_size;
if (_MemorySize == 0) chunk_size = DEFAULT_CHUNK_SIZE;
else chunk_size = 2 * _MemorySize; // 两倍速度分配新内存
_ptr_ChunkBegin = new char[chunk_size];
if (_ptr_ChunkBegin == 0) {
QMessageBox::critical(NULL,QObject::tr("内存不足"),QObject::tr("当前系统内存不足,无法继续运行,程序即将退出!"),QObject::tr("退出程序"));
exit(-1);
}
_ptr_ChunkEnd = _ptr_ChunkBegin + chunk_size;
_MemorySize += chunk_size;
_ptr_New = _ptr_ChunkBegin;
_vec_ChunkRecord.push_back(_ptr_ChunkBegin); // 保存,以便最后释放
}