高并发内存池(16)-三层缓存的回收过程
内存池的回收过程是内存管理系统的关键环节,它通过分层协作和智能合并机制,确保内存高效重复利用。以下是完整的回收流程解析:
一、回收触发场景
- ThreadCache回收:线程释放小对象(≤256KB)
- CentralCache回收:ThreadCache批量返还或Span完全空闲
- PageCache回收:CentralCache返还完整Span
二、三级回收流程
1. ThreadCache层级(无锁)
// ThreadCache释放单个对象
void Deallocate(void* ptr, size_t size) {FreeList* list = GetFreeList(size);list->Push(ptr); // 头插法插入自由链表// 如果自由链表过长,批量返还CentralCacheif (list->Size() >= list->MaxSize()) {void* start, *end;size_t batchNum = list->PopRange(start, end, list->MaxSize()/2);CentralCache::ReleaseListToSpans(start, size);}
}
特点:
- 线程本地操作无锁竞争
- 批量返还减少锁开销(如每次返还50%的对象)
- 自由链表采用头插法(O(1)时间复杂度)
2. CentralCache层级(桶锁)
// CentralCache接收ThreadCache返还的内存块
void 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::MapObjectToSpan(start); // 找到所属Span// 头插法归还到Span的自由链表NextObj(start) = span->_freeList;span->_freeList = start;span->_useCount--;// 如果Span完全空闲,返还PageCacheif (span->_useCount == 0) {_spanLists[index].Erase(span);PageCache::ReleaseSpanToPageCache(span); // 触发合并}start = next;}_spanLists[index]._mtx.unlock();
}
关键点:
- 桶锁粒度:不同size的SpanList有独立锁,减少竞争
- 引用计数:通过
_useCount
判断Span是否完全空闲 - 批量处理:一次处理多个对象,减少锁持有时间
3. PageCache层级(全局锁)
// PageCache合并空闲Span
void ReleaseSpanToPageCache(Span* span) {std::lock_guard<std::mutex> lock(_pageMtx);// 向前合并(检查前一页是否空闲)while (MergePrevSpan(span)) {}// 向后合并(检查后一页是否空闲)while (MergeNextSpan(span)) {}// 插入对应大小的SpanList_spanLists[span->_n].PushFront(span);// 更新页号映射(首尾页都指向该Span)_idSpanMap[span->_pageId] = span;_idSpanMap[span->_pageId + span->_n - 1] = span;
}
合并算法:
bool MergePrevSpan(Span* span) {PAGE_ID prevId = span->_pageId - 1;auto it = _idSpanMap.find(prevId);if (it == _idSpanMap.end()) return false;Span* prevSpan = it->second;if (prevSpan->_isUse || prevSpan->_n + span->_n > 128) return false;// 执行合并span->_pageId = prevSpan->_pageId;span->_n += prevSpan->_n;_spanLists[prevSpan->_n].Erase(prevSpan);delete prevSpan;return true;
}
合并效果:
[已用][空闲SpanA][空闲SpanB][已用]
合并后→ [已用][大空闲Span][已用]
三、回收过程示意图
sequenceDiagramparticipant ThreadCacheparticipant CentralCacheparticipant PageCacheThreadCache->>CentralCache: 批量返还对象(带桶锁)CentralCache->>Span: 更新自由链表和引用计数alt Span完全空闲CentralCache->>PageCache: 返还Span(全局锁)PageCache->>PageCache: 尝试前后页合并PageCache->>空闲链表: 插入合并后的Spanend
四、设计精髓
-
分层缓冲:
- ThreadCache:线程独占,无锁
- CentralCache:共享但分桶加锁
- PageCache:全局锁但操作频率低
-
合并策略:
- 立即合并:发现相邻空闲Span立即合并
- 双向检查:同时检查前后页,最大化合并机会
-
映射维护:
// 示例:合并后更新映射 _idSpanMap[newSpan->_pageId] = newSpan; // 首页 _idSpanMap[newSpan->_pageId + newSpan->_n - 1] = newSpan; // 尾页
- 确保通过任意页都能找到所属Span
-
锁粒度优化:
- ThreadCache:完全无锁
- CentralCache:按size分桶加锁
- PageCache:全局锁但操作较少
五、性能关键点
- 批量操作:
- ThreadCache攒够一批对象才返还CentralCache
- CentralCache批量操作Span的自由链表
- 合并触发时机:
- 只在Span完全空闲时尝试合并
- 避免频繁合并带来的开销
- 热点分离:
- 小对象高频操作走ThreadCache
- 大对象低频操作走PageCache
六、异常处理
- 跨线程释放:
- 对象必须由分配它的线程释放(TLS机制保证)
- 否则需要特殊处理(如加入全局释放队列)
- 内存泄漏检测:
- 通过
_useCount
判断未释放的内存 - 定期扫描
_idSpanMap
检查泄漏
- 通过
通过这种分层回收设计,内存池实现了:
- 高频小对象分配/释放:无锁操作,性能接近O(1)
- 大对象管理:系统级分配,避免碎片
- 自适应合并:智能平衡内存利用率与合并开销