赞
踩
专栏内容:
在平时运行时分配内存时,使用动态内存分配malloc/free或 new/delete,会直接从操作系统申请虚拟内存,释放时也归还给操作系统;
而操作系统会记录那些内存片段正在使用,那些已经释放;时间久后,内存碎片就越来越多,最终没法分配一片较大的内存出来,所以系统会尽量让空闲的片段连成一整片,不断的整理。
一般应用程序需要动态分配的次数和频率不高时,对操作系统的负担不重;
而像数据库这样的重型应用跑起来,会有大量的运行时动态内存的分配和释放,如果不对动态内存进行用户态的管理,必然会拖慢操作系统的运行,影响所有应用的性能。
本文就来分享一种在应用程序中进行动态内存管理的方法,减少向操作系统申请和释放的次数,来避免内存碎片整理的负担。
动态内存管理的目标主要有两个:
针对这两个目标,我们可以这样来做,先从内存的分配来看:
- 每次从操作系统申请一块较大的内存;单次数量多,次数就会少很多;
- 应用需要时,从这块大内存中切割划分,直到大块内存用尽,再重复1步骤;
- 对于少量一次需要很大的内存时,超过每次申请的上限,此时就直接从操作系统申请,这就按特例来处理。
这有点像买蛋糕,当一个人吃时,每次就买一小块;当过生日时,有很多人一起吃,那就一次性买个大蛋糕,回来切分。
我们也可以说算法来源于生活,而高于生活!
对于内存的释放回收,有两种策略:
重复利用策略
这种策略对于内存利用次数较高,与操作系统交互更少,但需要增加内存碎片的管理,也就是对于应用使用的不同大小的内存片,归还后要么进行移动合并,要么按大小进行排序再利用;
会增加碎片管理的负担,同时内存会有一些浪费。
整体归还策略
申请一大块内存,在应用内部进行切分为更小的片进行使用,当此大块内存上的分片均释放时,将此大块内存归还给操作系统。
这样避免内存碎片的整理,同时大块的释放也会减少操作系统内存碎片整理的负担;
但是也会存在内存浪费,当大块内存上有一小片一直不释放时,整块内存就会被浪费,迟迟得不到释放。
当然两种策略还可以再优化,但过多的优化,都会带来一些额外的开销。
动态内存管理的基本单位称为内存页(page),大小为4KB,也就是上节提到的大块内存,每次申请与释放都会按内存页来操作。
而对于应用程序来讲,它申请内存是从memPage中进行分配,每次分配一个memNode,包含申请的内存大小。
当一个MemPage用完或不够时,再从下一个memPage中分配。
所有的memPage采用单链表的形式串起来,方便释放时管理。
下面我们就分拆来了解一下,内存页的定义,以及内存的申请与释放的操作。
内存管理需要记录mempage的链表,同时为了更少的与操作系统交互,增加了一个freeList来记录释放的memPage,当freeList不空时,直接从这里分配即可。
内存管理结构的定义
#define MEMORY_POOL_MANAGER_VERSION (0x0B10)
typedef struct MemPoolManagerContext
{
int version;
unsigned long totalSize; /* 已经使用的动态内存大小 */
// SPINLOCK lock; /* protected this structure. */
DList memFreeList;
DList memPageList; /* memPageInfo list */
MemPageInfo *currUsePage;
}MemPoolManagerContext;
成员说明:
内存管页结构定义
内存页memPage作为向操作系统申请和释放的基本单位,它的定义如下:
#define MEMORY_POOL_PAGE_SIZE (4096)
#define MEMPAGE_INFO_LEN (sizeof(MemPageInfo))
typedef struct MemPageInfo
{
DList list;
int memPageSize;
int useOffset;
int freeSize;
int releaseSize;
}MemPageInfo;
typedef char *MemPage, *MemPtr;
成员说明:
这里新定义了一定类型MemPage, 也就是char *的别名,这样在后面分配内存页时使用,方便区分。
内存管节点结构定义
应用程序每次申请内存,都会分配一个内存节点结构,包含了申请的内存大小。
/*
* MemAlloc will alloc a MemBlock, and return MemBlock->ptr for user.
*/
typedef struct MemBlock
{
int size; /* memblock size + ptr[] size */
MemPageInfo *memPage; /* current memPageInfo pointer */
char ptr[];
}MemBlock;
成员说明:
注意,这里ptr必须定义为数组,因为它就是数据的首地址,也就是一段内存的开始;如果定义为指针,需要手动赋值;
内存页的分配
当MemPage不够分配时,都需要从操作系统中申请一块新的memPage。
先从FreeList中查找,如果找到,就添加到当前使用列表中;
如果没有空闲memPage时,直接调用malloc进行分配。
static int AddNewPageToContext(MemPoolManagerContext *poolContext) { MemPageInfo *currentMemPage = NULL; MemPage newPage = NULL; newPage = GetFreeMemPage(); if(NULL == newPage) { newPage = (MemPage)malloc(MEMORY_POOL_PAGE_SIZE); if(NULL == newPage) { exit(-1); } } currentMemPage = InitMemPage(newPage, MEMORY_POOL_PAGE_SIZE); /* add to context, and continue to alloc from context. */ AddMemPageToContext(currentMemPage, poolContext); return 0; }
添加到使用列表中,同时将使用中的内存总数进行累加。
static int AddMemPageToContext(MemPageInfo *memPage, MemPoolManagerContext *context)
{
context->totalSize += memPage->memPageSize;
/* add to mempage list */
AddMemPageNode(&(context->memPageList), memPage);
context->currUsePage = memPage;
return 0;
}
其中AddMemPageNode
是将新的内存页加到链表中,这个在链表章节介绍。
每次应用申请内存,都是从内存池中分配一个memNode。
当申请的size 小于当前内存页的空闲空间时,在当前页中分配一个memBlock结构;
static MemPtr AllocFromMemPage(MemPageInfo *mPage, int size)
{
MemBlock *memb = NULL;
mPage->freeSize -= size;
memb = (MemBlock*)((char *)mPage + mPage->useOffset);
mPage->useOffset += size;
memb->memPage = mPage;
memb->size = size;
return (MemPtr)(memb->ptr);
}
而当应用程序释放内存时,会将memBlock归还给对应的memPage;
在分配时,需要记录对应的memPage的地址,此时就可以进行引用。
static int ReleaseToMemPage(MemBlock *memb) { MemPageListInfo *memPageList = NULL; MemPageInfo *memPage = memb->memPage; DList *header = NULL; int ret = 0; if(NULL == memPage) { return -1; } memPage->releaseSize += memb->size; return 0; }
释放时只是将待释放的内存大小增加到了 releaseSize;
当然这里可以将再进行优化,当memPage为空时,将它从使用列表中移除,并填加到空闲列表中。
应用程序申请内存时,不能再使用malloc/free这两个接口了,需要使用自定义的接口。
动态内存申请接口
申请的流程如下:
#define MEMORY_POOL_BLOCK_HEADER_SIZE (sizeof(MemBlock)) #define MEMPAGE_INFO_LEN (sizeof(MemPageInfo)) #define MEMORY_POOL_MAX_ALLOC_SIZE (MEMORY_POOL_PAGE_SIZE - MEMPAGE_INFO_LEN) MemPtr AllocFromMemPool(unsigned int size) { MemPtr mem = NULL; MemPoolManagerContext *currentMemContext = NULL; MemPageInfo *currentMemPage = NULL; MemPage newPage = NULL; currentMemContext = GetMemPoolCurrentContext(); if(NULL == currentMemContext) { return NULL; } /* add memblock header size */ size += MEMORY_POOL_BLOCK_HEADER_SIZE; /* oversize */ if(size >= MEMORY_POOL_MAX_ALLOC_SIZE) { newPage = (MemPage)malloc(size); currentMemPage = InitMemPage(newPage, size); AddMemPageToContext(currentMemPage, currentMemContext); mem = AllocFromMemPage(currentMemPage, size); return mem; } do { mem = AllocFromMemContext(size, currentMemContext); if(NULL != mem) break; AddNewPageToContext(currentMemContext); }while(1); return mem; }
这里有一个小技巧,后面使用了一个do { }while(1)
的循环,其实这里就是为了写法简单,当没有空闲空间时,会申请一个内存页,然后继续分配内存节点。
动态内存释放接口
这里相对简单,就是将当前内存节点释放到对应的内存页上。
#define GetOffsetSize(type, member) (unsigned long)(&(((type *)(0))->member))
#define GetAddrByMember(memberaddr, member, type) (type *)(((char*)(memberaddr)) - GetOffsetSize(type,member))
#define GetMemBlockHeader(memptr) (GetAddrByMember(memptr, ptr, MemBlock))
int ReleaseToMemPool(MemPtr mem)
{
return ReleaseToMemPage(GetMemBlockHeader(mem));
}
在应用程序中拿到的是ptr数组首地址,通过它在结构体中的偏移,可以找到memBlock结构的地址,这里定义了一个宏。
当然可以使用offsetof
,这个预定义的函数来获取结构体成员的偏移。
在动态内存使用频繁的应用程序中,不仅与操作系统交互多,而且会造成大量的内存碎片,增加额外的系统负担。
本文分享了通过动态内存池的方法,每次申请一个内存页,然后在当有动态内存需要时,进行切分,可以避够内存碎片的产生。
当然也存在很多可优化的地方,在释放时可以保留一部分内存页在freeList中,这样进一步减少与操作系统的交互。
非常感谢大家的支持,在浏览的同时别忘了留下您宝贵的评论,如果觉得值得鼓励,请点赞,收藏,我会更加努力!
作者邮箱:study@senllang.onaliyun.com
如有错误或者疏漏欢迎指出,互相学习。
注:未经同意,不得转载!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。