项目篇----仿tcmalloc的内存池设计(内存回收)

1.Thread Cache 部分

我们基本的框架已经搭建完了,但是有一个问题现在,就是假设现在的thread cache里面归还回来了大量的内存,导致_freelist的桶里面都挂的很长很满了,这样下去我们的系统早晚崩溃,所以我们还要设计一个函数来解决这个问题:整体思路不难,这里直接放代码了

复制代码
void ThreadCache::Deallocate(void* ptr, size_t size)
{
	assert(ptr);
	assert(size <= MAX_BYTES);
	//找对映射的自由链表桶,对象插入进入
	size_t index = SizeClass::Index(size);
	_freeLists[index].Push(ptr);

	// 当链表长度大于一次批量申请的内存时就开始还一段list给central cache
	if (_freeLists[index].Size() >= _freeLists[index].MaxSize())
	{
		ListTooLong(_freeLists[index], size);
	}
}

void ListTooLong(FreeList& list, size_t size)
{
	void* start = nullptr;
	void* end = nullptr;
	list.PopRange(start, end, list.MaxSize());  //这个函数start指向要归还的_freelist
	CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}

具体看一下那个PopRange函数的实现方法:

复制代码
	void PopRange(void*& start, void*& end, size_t n)
	{
		//链表操作,自己画个图就明白了
		assert(n <= _size);
		start = _freelist;
		end = start;

		for (size_t i = 0; i < n; i++)
		{
			end = NextObj(end);
		}
		_freelist = NextObj(end);
		NextObj(end) = nullptr;
		_size -= n;
	}

这里为了方便管理,我们可以在freelist类中加上_size的私有成员,用于实时记录size大小,之后记得把所有的pop或者push都维护一下这个_size,记得维护这个后加进来的变量!!!,这里代码就先不给出了,很简单!

2.Central Cache部分

thread cache的内存回到cental cache时,我们要将其合并,但是此时返回的obj对象我们还不知道其隶属于那个span,所以要先写一个函数来实现这个功能:

复制代码
Span* PageCache::MapObjectToSpan(void* obj)
{
	PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
	auto ret = _idSpanMap.find(id);  //<PAGE_ID, Span*>
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		//肯定能找到,找不到就是你前面哪里错了
		assert(false);
		return nullptr;
	}
}

找到这个对象后,头插法归还到 Span 的自由链表,当pagecache==0时,说明span的切分出去的所有小块内存都回来了,这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并.

在归还前有个细节操作要注意:先将该span剥离出来, 从 Central Cache 对应 size class 的双向循环链表 中移除,清空 _freelist,

因为当 _useCount == 0 时,说明这个 Span 的所有小块内存都已经归还了 。此时:Span 即将被移交给 Page Cache ,Page Cache 管理的是整页的、未切分的大块内存, 不需要再维护小块的自由链表了,实际上,这个 Span 的所有内存块(包括刚归还的)都还在 Span 管理的物理内存范围内。清空 _freelist 只是重置管理结构 ,内存本身并没有丢失。随后将 Span 从 Central Cache 的链表中彻底剥离 ,变成孤立节点,前后变为空:

示意图:

之后归还的时候记得切换锁~

代码如下:

复制代码
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanLists[index]._mtx.lock();
	while (start)
	{
		void* next = NextObj(start);
		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
		//头插法归还到 Span 的自由链表
		NextObj(start) = span->_freelist;
		span->_freelist = start;

		span->_useCount--;

		// 说明span的切分出去的所有小块内存都回来了
		// 这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并
		if (span->_useCount == 0)
		{
			_spanLists[index].Erase(span);
			span->_freelist = nullptr;
			span->_next = nullptr;
			span->_Prev = nullptr;

			// 释放span给page cache时,使用page cache的锁就可以了
			// 这时把桶锁解掉
			_spanLists[index]._mtx.unlock();
			PageCache::GetInstance()->_pageMX.lock();
			PageCache::GetInstance()->ReleaseSpanToPageCache(span);
			PageCache::GetInstance()->_pageMX.unlock();

			_spanLists[index]._mtx.lock();
		}
		start = next;
	}
	_spanLists[index]._mtx.unlock();
}

3.Span Cache部分

当陆陆续续的一块一块的span都回来时,我们需要将其进行合并,未合并之前的span可能会存在内碎片和外碎片,内碎片倒是无所谓,大不了就是暂时回不来,等用它的线程结束了他就回来了就能进行分配了,但是外碎片却很棘手,因为这会导致一个问题产生,就是假设你想申请8byte的空间,系统显示也有8byte,但是就是开不出来因为不连续,成外碎片了,因此我们要间相邻的外碎片以及与空闲的大块span合并到一起!

代码思路就是:

采用双向边界合并策略回收内存页:首先向前遍历合并相邻空闲页,再向后遍历合并,通过页号哈希表快速定位邻接块,最终将合并后的连续大内存块按页数分类插入链表,并更新边界映射,代码比较简单而且写了详细的注释,这里不多说了~

代码如下:

复制代码
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	// 对span前后的页,尝试进行合并,缓解内存碎片问题(主要是外碎片的问题)
	//向前合并
	while (1)
	{
		PAGE_ID prevId = span->_pageId - 1;
		auto ret = _idSpanMap.find(prevId);
		if (ret == _idSpanMap.end())
		{
			//没找到,前面页号没有,不合并了
			break;
		}
		// 前面相邻页的span在使用,不合并了
		Span* prevSpan = ret->second;
		if (prevSpan->_IsUse == true)//正在使用
		{
			break;
		}
		// 合并出超过128页的span没办法管理,不合并了
		if (prevSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}
		//下面就可以合并了
		span->_pageId = prevSpan->_pageId;
		span->_n += prevSpan->_n;

		//记得将原来的prevspan删掉,已经合并了
		_spanList[prevSpan->_n].Erase(prevSpan);
		delete prevSpan;
	}
	//向后合并
	while (1)
	{
		PAGE_ID nextId = span->_pageId + span->_n;  
		//这里区分前后,前只需-1即可获取前面的prevId,后则需要跨过本段span的页数,
		//pageid可以理解为这一大块的id都是_pageid,而_n是页数
		auto ret = _idSpanMap.find(nextId);
		if (ret == _idSpanMap.end())
		{
			break;
		}
		//找到了
		Span* nextspan = ret->second;
		if (nextspan->_IsUse == true)
			break;
		if (nextspan->_n + span->_n > NPAGES - 1)
			break;
		//此时可以合并了
		span->_n += nextspan->_n;

		_spanList[nextspan->_n].Erase(nextspan);
		delete nextspan;
	}

	//此时前后页合并完成了,进行整理
	_spanList[span->_n].PushFront(span);
	span->_IsUse = false;
	_idSpanMap[span->_pageId] = span;    //// 起始页映射
	_idSpanMap[span->_pageId+span->_n - 1] = span;// 结束页映射
}

4.细节修订,开辟size大小

如果我们一次性开辟小于256k,则直接走这三个cache就行,那要是大于呢?我们要对这种情况进行代码编写和处理:

注:这里的new 和delete先看上面的被注释掉的版本,下面的那个一会会说为什么这么改!

第一处要改造的地方是获取k页span那里:

就是当k大于最大页数129-1=128时,要处理一下

释放那里也要对这个进行改一下:

之后分配函数roundUP函数那里也要搞一下,加上这个情况:

之后再并行测试那里也加一下:

5.摆脱new 和 delete的依赖

我们这个内存池还有一些问题,就是这个是仿照tcmalloc来设计的,而tcmalloc设计的初衷就是要彻底摆脱对底层 malloc 的依赖,实现自包含的高性能内存管理 。因此TCMalloc 要替代 malloc,内部不能调用 malloc, 否则就会造成左右脑互博的局面,就像现在部分xxn一样,既要独立又要自由,这是不可取的!!!

但是我们的项目里用的都是new 和 delete 啊,所以要进行整改,正好我们前面写过定长内存池的代码,我们可以通过引入定长内存池,在我new span或者delete span的时候进行改造,这样就能避免对malloc的依赖,因为定长内存池的内存我们是直接用systemalloc直接从系统要的!

这里回顾一下定长内存池的New函数和Delete函数

NEW:

复制代码
	T* New()
	{
		//先定义obj对象
		T* obj = nullptr;
		//申请内存,先从freelist上面申请,不够了再去开大块空间
		if (_freelist)
		{
			//有还回来的空间,且空间足够所需,
			//"头删"一块内存给obj对象
			void* next = *(void**)_freelist;
			obj = (T*)_freelist;
			_freelist = next;
		}
		else
		{
			// 剩余内存不够一个对象大小时,则重新开大块空间
			if (_remainbytes < sizeof(T))
			{
				_remainbytes = 128*1024;//向大内存申请空间
				//_memory = (char*)malloc(_remainbytes);
				_memory = (char*)SystemAlloc(_remainBytes >> 13);
				if (_memory == nullptr)//检测申请成功与否
				{
					throw std::bad_alloc();
				}
			}
			//内存现在申请成功了,准备给Obj对象分配
			obj = (T*)_memory;
			size_t ObjSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			//解释objsize:
			// 空间返回给用户,是给用户使用。用户给我们,我们是用这块空间的前4个或者8个字节,然后指向后面的一个节点空间
			//那如果T是char  这块空间就会不够用了,所以一定要有这个大小,然后才可以实现我们的这个思想。
			_memory += ObjSize;
			_remainbytes -= ObjSize;
		}
		// 定位new,显示调用T的构造函数初始化
		new(obj)T;
		return obj;
	}

Delete:

复制代码
	void Delete(T* obj)
	{
		// 显示调用析构函数清理对象
		// 1. 显式调用析构函数(清理对象内部资源)
        // 2. 内存不释放,而是归还到内存池的自由链表

		obj->~T();
		//归还的内存以头插的方式由freelist管理
		//头插
		*(void**)obj = _freelist;
		_freelist = obj;
	}

我们在span cache这里引入定长内存池,之后把所有的new都换成通过_spanpool来调用自己的New函数,delete同理

如下图:

相关推荐
全马必破三2 小时前
Vue 和 React 的区别
前端·vue.js·react.js
weixin_421994782 小时前
依赖注入与中间件 - ASP.NET Core 核心概念
后端·中间件·asp.net
东东5162 小时前
ssm机场网上订票系统 +VUE
java·前端·javascript·vue.js·毕设
倚肆2 小时前
Kafka 生产者与消费者配置详解
java·分布式·后端·kafka
无巧不成书02182 小时前
React Native 鸿蒙开发(RNOH)深度适配
前端·javascript·react native·react.js·前端框架·harmonyos
Honmaple2 小时前
Cherry Studio API完整参考手册(实操版)
后端
pas1362 小时前
01-vite 学习内容
前端·webpack
p***19942 小时前
SpringBoot项目中替换指定版本的tomcat
spring boot·后端·tomcat
Coder_Boy_2 小时前
【Java核心】企业级高并发系统底层设计思想
java·前端·数据库·spring boot·高并发