> 技术文档 > 高并发内存池(16)-三层缓存的回收过程

高并发内存池(16)-三层缓存的回收过程


高并发内存池(16)-三层缓存的回收过程

内存池的回收过程是内存管理系统的关键环节,它通过分层协作智能合并机制,确保内存高效重复利用。以下是完整的回收流程解析:


一、回收触发场景

  1. ThreadCache回收:线程释放小对象(≤256KB)
  2. CentralCache回收:ThreadCache批量返还或Span完全空闲
  3. PageCache回收:CentralCache返还完整Span

二、三级回收流程

1. ThreadCache层级(无锁)
// ThreadCache释放单个对象void Deallocate(void* ptr, size_t size) { FreeList* list = GetFreeList(size); list->Push(ptr); // 头插法插入自由链表 // 如果自由链表过长,批量返还CentralCache if (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完全空闲,返还PageCache if (span->_useCount == 0) { _spanLists[index].Erase(span); PageCache::ReleaseSpanToPageCache(span); // 触发合并 } start = next; } _spanLists[index]._mtx.unlock();}

关键点

  • 桶锁粒度:不同size的SpanList有独立锁,减少竞争
  • 引用计数:通过_useCount判断Span是否完全空闲
  • 批量处理:一次处理多个对象,减少锁持有时间

3. PageCache层级(全局锁)
// PageCache合并空闲Spanvoid ReleaseSpanToPageCache(Span* span) { std::lock_guard 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][已用]

三、回收过程示意图

sequenceDiagram participant ThreadCache participant CentralCache participant PageCache ThreadCache->>CentralCache: 批量返还对象(带桶锁) CentralCache->>Span: 更新自由链表和引用计数 alt Span完全空闲 CentralCache->>PageCache: 返还Span(全局锁) PageCache->>PageCache: 尝试前后页合并 PageCache->>空闲链表: 插入合并后的Span end

四、设计精髓

  1. 分层缓冲

    • ThreadCache:线程独占,无锁
    • CentralCache:共享但分桶加锁
    • PageCache:全局锁但操作频率低
  2. 合并策略

    • 立即合并:发现相邻空闲Span立即合并
    • 双向检查:同时检查前后页,最大化合并机会
  3. 映射维护

    // 示例:合并后更新映射_idSpanMap[newSpan->_pageId] = newSpan; // 首页_idSpanMap[newSpan->_pageId + newSpan->_n - 1] = newSpan; // 尾页
    • 确保通过任意页都能找到所属Span
  4. 锁粒度优化

    • ThreadCache:完全无锁
    • CentralCache:按size分桶加锁
    • PageCache:全局锁但操作较少

五、性能关键点

  1. 批量操作
    • ThreadCache攒够一批对象才返还CentralCache
    • CentralCache批量操作Span的自由链表
  2. 合并触发时机
    • 只在Span完全空闲时尝试合并
    • 避免频繁合并带来的开销
  3. 热点分离
    • 小对象高频操作走ThreadCache
    • 大对象低频操作走PageCache

六、异常处理

  1. 跨线程释放
    • 对象必须由分配它的线程释放(TLS机制保证)
    • 否则需要特殊处理(如加入全局释放队列)
  2. 内存泄漏检测
    • 通过_useCount判断未释放的内存
    • 定期扫描_idSpanMap检查泄漏

通过这种分层回收设计,内存池实现了:

  • 高频小对象分配/释放:无锁操作,性能接近O(1)
  • 大对象管理:系统级分配,避免碎片
  • 自适应合并:智能平衡内存利用率与合并开销

在线教育技巧