当用户将从线程资源层申请的内存块返还给线程资源层后,一系列复杂的回收工程就开始了。
一、ThreadCache层的回收
当用户调用内存块释放接口时,直接将内存块插入回线程资源层的对应桶里即可。
二、CentreCache层的回收
当ThreadCache层的回收工作的结尾处,做一个检测,当检测到回收了内存块的ThreadCache的桶里的内存块数量达到一定值时(可以是MaxSize的值和其它更精细的方案,如检测整个ThreadCache层的资源是否超标),就调用中心资源层的接口,将桶里的全部内存块回收到中心资源层对应桶里的Span块内。
注意,每个内存块都可能属于不同的Span包,那么怎么找到内存块原本属于的Span包是哪个呢?(必须归还回原本属于的Span包,这样才能进一步归还回页缓存资源层)
中心资源层的Span包来自于页缓存资源层,它们内存放的是一页一页的内存,实际上可以在页缓存资源层将Span包分配给中心资源层时,就将Span包内的每一页的页号与Span包对应起来(包括被分割后插入回PageCache的Span包)。那么只需要由内存块的地址计算出页号,再根据页号查找出其所属的Span包即可了。
可以借助unordered_map(哈希map)来完成这个页号与所属Span块的对照表:
std::unordered_map<PAGE_ID, Span*> Page_Span;
三、PageCache层的回收
PageCache层的回收要复杂一些,因为它还要负责完成Span包的合并。在01.整体框架设计
中有过介绍:
其中所谓的“会将相邻的空闲页进行合并”其实就是Span包的合并。
通过上一节04 页缓存资源层的初步设计的介绍可知,PageCache每次都只会向系统申请一个多达128页的超大Span包,PageCache内的各个Span包都是由这个超大Span包分割而来的。那么问题来了,假使一个128页的Span包被分割成一个60页的Span包和68页的Span包,下次要申请一个100页的Span包了该怎么办呢?如果重新向系统申请一个128页的超大Span包就太浪费了。
因此, 在CentreCache层的回收工作的结尾处也要做一个检测,即检测到某个Span包内的内存块已经全部回收后,就将整个Span包回收给PageCache。PageCache接收到Span包后,并不直接将之插入进对应的页桶里去,而是先试着与临近的左右页桶合并,试图合并成包含更多页的大Span包,直到不能再合并为止,最后才将该Span包插入回对应大小的页桶里去。
这是PageCache的逻辑结构:
但实际上,在内存空间中,每个Span包都是紧挨着的,仅靠页号定义一个Span的头,含有的页数来定义一个Span的尾。
那么假设上图中占有第125页到135页的Span包从CentreCache层被归还,就可以一直向前合并到第120页,再向后合并到第149页,组成一个多达29页的Span包,并将之插入回29号页桶。
Span包合并的过程其实相当简单,只需要根据本Span包的前一页或后一页的页号,从unordered_map<PAGE_ID, Span*> Page_Span中查找到该页所属的Span包,然后将本Span页的起始页号修改成新Span的页号,所含页数修改为新Span的页数,然后删除被合并的Span即可。
在以下三种情况下,Span包应立即停止合并:
- 前一个Span包或后一个Span包的is_used字段值为true,代表该Span包正在被上层使用,应停止本方向的合并。
- 没有从unordered_map<PAGE_ID, Span*> Page_Span中找到前一页或后一页所属的Span包。意味着,该页已经不在系统分配给本内存池的内存区域中,应停止本方向的合并。
- 如果进行合并,合并后的Span包大小将超过128页(两个超大128页原始Span正好完全挨着的情况),这超过了PageCache层管理的上限,应停止本方向的合并。
通过三层资源的有序回收,有效地缓解了内存池中会出现内存碎片,导致小内存块太多,没有大内存块可用的问题。