1. 什么是内存池?
1.1 池化技术
池是一种设计模式,将程序中需要经常使用的核心资源先申请出来,放到一个池内,由程序自己管理,提高资源的使用效率,保证本程序占有的资源数量。经常使用的池技术包括内存池、线程池和连接池等,其中内存池和线程池使用最多。
1.2 内存池
内存池(Memory Pool) 是一种动态内存分配与管理技术。 通常情况下,程序员习惯直接使用 new、delete、malloc、free 等API申请分配和释放内存,这样导致的后果是:当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。
内存池在真正使用内存之前,先申请分配一大块内存(内存池)留作备用,当程序员申请内存时,从池中取出一块动态分配,当程序员释放内存时,将释放的内存再放入池内,再次申请池可以再取出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。
2. 高并发内存池(Concurrency Memory Pool)
2.1 解决问题
- 解决内存碎片问题;
- 解决性能问题;
- 并发性能。
2.2 组成
高并发内存池主要由3部分组成: thread cache(线程缓存)、central cache(中心缓存)、page cache(页缓存)。
-
thread cache:线程缓存是每个线程独有的,不需要加锁的;
-
central cache:中心缓存是所有线程所共享,均衡按需调度;
-
page cache:页缓存所存储的内存是以页为单位存储及分配的,缓解内存碎片的问题。
【整体框架流程图】
3.3 详解
3.3.1 第一层 thread cache
-
thread cache:线程缓存是每个线程独有的,用于小于64k的内存的分配,每个线程独享一个cache,因此thread cache中申请或回收内存是不需要加锁的;
-
若申请内存大于64k? -》 向page cache申请;
-
thread cache本质是由一个哈希映射的对象自由链表构成。
【申请内存】
-
当内存申请size <= 64k时在thread cache中申请内存,计算size在自由链表中的位置,如果自由链表中有内存对象时,直接从FistList[i]中pop一下对象,时间复杂度是O(1),且没有锁竞争;
-
当FreeList[i]中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表并返回一个对象。
【释放内存】
-
当释放内存小于64k时将内存释放回thread cache,计算size在自由链表中的位置,将对象push到FreeList[i];
-
当链表的长度过长,则回收一部分内存对象到central cache。
class ThreadCache
{
public:
// 申请内存和释放内存
void* Allocte(size_t size);
void Deallocte(void* ptr, size_t size);
// 从中心缓存获取对象
void* FetchFromCentralCache(size_t index);
// 如果自由链表中对象超过一定长度就要释放给中心缓存
void ListTooLong(FreeList& freeList, size_t num, size_t size);
private:
FreeList _freeLists[NFREE_LIST];
};
// 线程TLS Thread Local Storage
_declspec (thread) static ThreadCache* pThreadCache = nullptr;
3.3.2 第二层 central cache
-
central cache:中心缓存是所有线程所共享,均衡按需调度。thread cache是按需从central cache中获取的对象,central cache周期性的回收 thread cache中的对象,避免一个线程占用了太多的内存,而造成其他线程的内存吃紧。central cache是存在竞争的,所以central cache中申请或回收内存是需要加锁的;
-
central cache本质是由一个哈希映射的span对象自由链表构成;
-
设计成了单例模式,保证全局只有唯一的central cache;
-
span:双向、带头、循环链表,大小为一页。
【申请内存】
-
当thread cache中没有内存时,就会批量向central cache申请一些内存对象,central cache有一个哈希映射的freelist,freelist中挂着span,从span中取出对象给thread cache,这个过程是需要加锁的;
-
central cache中没有非空的span时,则将空的span链在一起,向page cache申请一个span对象,span对象中是一些以页为单位的内存,切成需要的内存大小,并链接起来,挂到span中;
-
central cache的span中有一个use_count,分配一个对象给thread cache,就++use_count。
【释放内存】
- 当thread cache过长或者线程销毁,则会将内存释放回central cache中的,释放回来时 --use_count;
- 当use_count减到0时则表示所有对象都回到了span,则将span释放回page cache,page cache中会对前后相邻的空闲页进行合并。
struct Span //跨度类
{
PAGE_ID _pageid = 0; // 页号
PAGE_ID _pagesize = 0; // 页的数量
FreeList _freeList; // 对象自由链表
size_t _objSize = 0; // 自由链表对象大小
int _usecount = 0; // 内存块对象使用计数
Span* _next = nullptr; //链接span前后
Span* _prev = nullptr;
};
class CentralCache
{
public:
// 从中心缓存获取一定数量的对象给thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t num, size_t size);
// 将一定数量的对象释放到span跨度
void ReleaseListToSpans(void* start, size_t size);
// 从spanlist 或者 page cache获取一个span
Span* GetOneSpan(size_t size);
static CentralCache& GetInsatnce()
{
static CentralCache inst;
return inst;
}
private:
//单例:保证创建出一个对象
CentralCache()
{}
CentralCache(const CentralCache&) = delete;
// 中心缓存span双向自由链表
SpanList _spanLists[NFREE_LIST];
};
3.3.3 第三层 page cache
-
page cache:页缓存所存储的内存是以页为单位存储及分配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给central cache。page cache会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。
-
申请内存大于128页? -》 向系统申请;
-
设计成了单例模式,保证全局只有唯一的page cache;
-
page cache是一个以页为单位的span自由链表。
【申请内存】
-
当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个;
- 例如:申请的是4page,4page后面没有挂span,则向后面寻找更大的span,假设在10page位置找到一个span,则将10page span分裂为一个4page span和一个6page span。
-
如果找到128 page都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式申请128page span挂在自由链表中,再重复上述中的过程。
【释放内存】
- 如果central cache释放回一个span,则依次寻找span的前后page id的span,看是否可以合并,如果合并继续向前寻找。则可以将切小的内存合并收缩成大的span,减少内存碎片。
class PageCache
{
public:
// 申请一个新span
Span* _NewSpan(size_t numpage);
Span* NewSpan(size_t numpage);
// 释放空闲span回到Pagecache,并合并相邻的span
void ReleaseSpanToPageCache(Span* span);
Span* GetIdToSpan(PAGE_ID id);
static PageCache& GetPageCacheInstance()
{
static PageCache inst;
return inst;
}
private:
// 单例模式
PageCache()
{}
PageCache(const PageCache&) = delete;
SpanList _spanLists[MAX_PAGES];
std::unordered_map<PAGE_ID, Span*> _idSpanMap;
std::mutex _mtx;
};
3.3.4 系统
- 申请内存大于128时,找系统申请;
- windows:VirtualAlloc;linux:brk、mmap等。
inline static void* SystemAlloc(size_t num_page) //向系统申请内存
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, num_page*(1 << PAGE_SHIFT), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
// brk mmap等
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}