高并发内存池(14)- PageCache回收内存
高并发内存池(14)- PageCache回收内存
这段代码是页缓存(PageCache) 中用于释放空闲Span并进行内存合并的关键函数,主要目的是减少内存碎片。
函数功能
ReleaseSpanToPageCache
完成两个核心任务:
- 合并相邻的空闲Span:向前和向后查找可合并的Span
- 将合并后的Span重新插入页缓存:更新所有管理信息
代码分段解析
1. 向前合并(合并前面的空闲Span)
while (1)
{PAGE_ID prevId = span->_pageId - 1; // 前一个页号auto ret = _idSpanMap.find(prevId);// 三种情况停止合并:if (ret == _idSpanMap.end()) break; // 1. 前面没有Spanif (prevSpan->_isUse == true) break; // 2. 前面的Span正在使用if (prevSpan->_n + span->_n > NPAGES-1) break; // 3. 合并后会超过最大管理大小// 执行合并span->_pageId = prevSpan->_pageId; // 更新起始页号span->_n += prevSpan->_n; // 增加页数_spanLists[prevSpan->_n].Erase(prevSpan); // 从原位置移除delete prevSpan; // 释放被合并的Span
}
示例:
现有Span: [pageId=100, n=3]
发现前一个Span: [pageId=99, n=2](空闲)
合并后: [pageId=99, n=5]
2. 向后合并(合并后面的空闲Span)
while (1)
{PAGE_ID nextId = span->_pageId + span->_n; // 后一个页号auto ret = _idSpanMap.find(nextId);// 三种停止条件(同向前合并)if (ret == _idSpanMap.end()) break;if (nextSpan->_isUse == true) break;if (nextSpan->_n + span->_n > NPAGES-1) break;// 执行合并(只需增加页数,起始页号不变)span->_n += nextSpan->_n;_spanLists[nextSpan->_n].Erase(nextSpan);delete nextSpan;
}
示例:
现有Span: [pageId=100, n=3]
发现后一个Span: [pageId=103, n=2](空闲)
合并后: [pageId=100, n=5]
3. 最终处理合并后的Span
_spanLists[span->_n].PushFront(span); // 按新大小插入对应桶
span->_isUse = false; // 标记为空闲// 更新页号映射(首尾页都指向这个Span)
_idSpanMap[span->_pageId] = span;
_idSpanMap[span->_pageId + span->_n - 1] = span;
关键设计思想
1. 内存碎片整理
通过合并相邻空闲Span,将多个小Span合并为一个大Span,从而:
- 减少外部碎片:提高大块连续内存的可用性
- 提高内存利用率:避免小碎片无法被利用
2. 分级管理
合并后的Span会根据新的大小_n
被放入对应的桶(_spanLists[_n]
),保持:
- 小Span(1-128页)在对应桶中
- 大Span(>128页)直接由系统管理
3. 映射维护
_idSpanMap[span->_pageId] = span; // 首页映射
_idSpanMap[span->_pageId + span->_n - 1] = span; // 尾页映射
确保通过任意一页都能找到整个Span。
合并过程可视化
假设当前状态:
- 页号100-104:已使用
- 页号105-107:空闲SpanA(3页)
- 页号108-109:空闲SpanB(2页)
合并流程:
- 释放一个Span [105-107]
- 向后合并发现SpanB [108-109]:
- 合并为 [105-109](5页)
- 向前合并检查页号104(假设正在使用,停止)
最终得到大Span [105-109],放入_spanLists[5]
桶。
为什么需要这个函数?
解决内存碎片问题
频繁分配释放会导致:
内存状态: [已用][空闲][已用][空闲][已用]
通过合并:
合并后: [已用][大空闲块][已用]
提升分配效率
大块连续内存可以:
- 更快满足大内存请求
- 减少向操作系统申请的次数
支持内存重用
合并后的Span可以再次被切分用于不同大小的请求。
在内存回收流程中的角色
graph TDA[CentralCache释放Span] --> B[调用ReleaseSpanToPageCache]B --> C{尝试向前合并}C -->|成功| D[更新Span信息]C -->|失败| E{尝试向后合并}E -->|成功| F[更新Span信息]E -->|失败| G[直接插入对应桶]D --> H[更新映射关系]F --> HG --> H
这个函数是内存池保持高效的关键,通过智能的合并策略有效缓解了内存碎片问题。