首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >高并发内存池回收机制深度解析

高并发内存池回收机制深度解析

作者头像
小文要打代码
发布2025-07-13 08:51:30
发布2025-07-13 08:51:30
2200
举报
文章被收录于专栏:C++学习历程C++学习历程

项目源码:https://gitee.com/kkkred/thread-caching-malloc

引言

在高并发场景中,内存分配与回收的效率直接影响系统吞吐量。传统单内存池因全局锁和碎片问题,难以满足高性能需求。而分层内存池框架(ThreadCache、CentralCache、PageCache)通过「线程本地回收+全局协调+系统级释放」的三级回收机制,实现了高效的内存管理。本文将深入解析这三层组件的回收逻辑、关键设计点及协作流程,揭示高并发场景下内存回收的「无锁化」与「碎片控制」之道。


一、ThreadCache回收:线程本地的「无锁快速回收」

ThreadCache作为线程私有的内存池,负责高频小对象(≤1KB)的回收。其核心目标是​​无锁、高效​​,避免多线程竞争导致的性能损耗。

1.1 回收流程:本地链表直接操作

ThreadCache的回收操作完全在本地线程完成,无需全局锁。其核心步骤如下:

1.1.1 对象回收到空闲链表

当线程调用thread_cache_free释放对象时,首先根据对象大小计算哈希桶索引(对齐后的Size Class),然后将对象指针插入对应空闲链表的头部。

​代码示例​​:

代码语言:javascript
复制
// ThreadCache释放函数(核心回收逻辑)
void thread_cache_free(void* ptr, size_t size) {
    if (!tls_thread_cache || !ptr) return;

    // 1. 计算哈希桶索引(对齐后的Size Class)
    size_t aligned_size = align_to_power_of_two(size);
    int class_index = get_size_class_index(aligned_size);
    if (class_index < 0 || class_index >= MAX_SIZE_CLASS) {
        class_index = MAX_SIZE_CLASS - 1;  // 保险处理
    }
    void** bucket = &tls_thread_cache->buckets[class_index];

    // 2. 插入空闲链表头部(O(1)时间)
    *(void**)ptr = *bucket;  // 将新对象插入链表头部
    *bucket = ptr;           // 更新链表头指针
}
1.1.2 本地缓存满时的批量回收

ThreadCache的空闲链表容量有限(如每个Size Class最多缓存16个对象)。当本地缓存满时,需将多余对象批量回收到CentralCache,避免内存浪费。

​触发条件​​:

  • 本地空闲链表长度 ≥ MAX_LOCAL_CACHE_SIZE(如16);
  • 或对象大小超过阈值(如1KB,需跨线程共享)。

​回收逻辑​​:

代码语言:javascript
复制
// ThreadCache批量回收函数
void thread_cache_flush_to_central(ThreadCache* cache) {
    for (int i = 0; i < MAX_SIZE_CLASS; i++) {
        void** bucket = &cache->buckets[i];
        if (*bucket == NULL) continue;

        // 加锁保护CentralCache的全局操作
        pthread_mutex_lock(&central_cache.lock);
        
        // 将本地空闲链表中的对象转移到CentralCache的空闲链表
        void* obj = *bucket;
        while (obj) {
            void* next = *(void**)obj;
            central_cache_add_free_object(&central_cache, obj, i);
            obj = next;
        }
        *bucket = NULL;  // 清空本地链表
        
        pthread_mutex_unlock(&central_cache.lock);
    }
}
1.2 关键设计:无锁与本地化
  • ​无锁操作​​:回收仅需修改本地链表头指针,无需加锁,避免多线程竞争;
  • ​本地缓存​​:高频小对象优先在本地回收,减少跨线程通信开销;
  • ​批量回收​​:本地缓存满时批量转移,降低CentralCache的访问频率。

二、CentralCache回收:全局共享的「Span合并与分级回收」

CentralCache负责中大对象(>1KB)的回收,通过​​Span结构​​和​​双链表​​实现高效的内存块管理。其核心挑战是​​碎片控制​​和​​跨线程协调​​。

2.1 回收流程:从对象到Span的聚合

当ThreadCache或应用程序调用central_cache_free释放对象时,CentralCache需将对象转换为Span,并合并相邻的空闲Span以减少碎片。

2.1.1 对象映射到Span

每个对象在分配时会被关联到一个Span(记录其所属的内存块)。释放时,通过对象地址找到对应的Span,并将其标记为空闲。

​数据结构支持​​:

  • ​对象到Span的映射表​​:使用哈希表记录每个对象地址对应的Span指针(键为对象地址,值为Span指针);
  • ​Span元数据​​:包含起始页号、总页数、状态(空闲/已分配)等信息。

​代码示例​​:

代码语言:javascript
复制
// CentralCache的Span映射表(哈希表)
typedef struct {
    void* obj_addr;       // 对象地址
    Span* span;           // 对应的Span指针
} ObjectToSpanMap;

// 对象释放时查找对应的Span
Span* find_span_by_object(void* obj_addr) {
    ObjectToSpanMap* entry = hash_table_lookup(&central_cache.map, obj_addr);
    return entry ? entry->span : NULL;
}
2.1.2 空闲Span的合并与分级回收

释放对象后,CentralCache会检查相邻的Span是否空闲。若相邻,则合并为更大的Span(减少碎片);若合并后的Span足够大(如超过页大小的2倍),则将其回收到PageCache。

​合并逻辑​​:

代码语言:javascript
复制
// 合并相邻的空闲Span
void merge_adjacent_spans(CentralCache* cache, Span* span) {
    // 检查前驱Span是否空闲
    if (span->prev && span->prev->is_free) {
        // 合并前驱Span
        span->start_page = span->prev->start_page;
        span->page_count += span->prev->page_count;
        // 从空闲链表中移除前驱Span
        remove_span_from_free_list(cache, span->prev);
        free(span->prev);  // 释放前驱Span的元数据
    }

    // 检查后继Span是否空闲(类似逻辑)
    // ...
}

// 分级回收:将大Span回收到PageCache
void central_cache_recycle_to_page_cache(CentralCache* cache, Span* span) {
    if (span->page_count >= PAGE_SIZE_THRESHOLD) {  // 如超过4页(16KB)
        // 从CentralCache的空闲链表中移除
        remove_span_from_free_list(cache, span);
        
        // 回收到PageCache(调用PageCache的回收接口)
        page_cache_recycle_span(cache->page_cache, span);
    } else {
        // 保留在CentralCache的空闲链表中
        add_span_to_free_list(cache, span);
    }
}
2.2 关键设计:Span合并与分级策略
  • ​Span合并​​:通过双链表快速检查相邻Span,合并后减少内存碎片;
  • ​分级回收​​:根据Span大小决定是否回收到PageCache(大Span减少CentralCache的内存压力);
  • ​哈希表映射​​:通过对象地址快速定位Span,避免遍历链表查找。

三、PageCache回收:系统级的「内存归还与延迟释放」

PageCache是内存池与操作系统的桥梁,负责大内存块(页级及以上)的回收。其核心目标是​​减少系统调用​​和​​优化内存利用率​​。

3.1 回收流程:从Span到系统页的释放

当CentralCache需要回收大内存块时,会调用page_cache_recycle_span将Span转换为系统页,并归还给操作系统。

3.1.1 Span到系统页的转换

Span记录了内存块的起始页号和总页数。PageCache根据这些信息,计算对应的虚拟地址范围,并调用munmap释放内存。

​代码示例​​:

代码语言:javascript
复制
// PageCache回收Span函数
void page_cache_recycle_span(PageCache* cache, Span* span) {
    if (!span || !span->is_free) return;

    // 1. 计算虚拟地址范围(起始地址到结束地址)
    void* start_addr = page_to_addr(span->start_page);
    void* end_addr = (char*)start_addr + span->page_count * PAGE_SIZE;

    // 2. 调用munmap释放内存(系统调用)
    int ret = munmap(start_addr, span->page_count * PAGE_SIZE);
    if (ret == 0) {
        // 释放成功,更新PageCache的统计信息
        cache->total_pages -= span->page_count;
        cache->free_pages -= span->page_count;
        free(span);  // 释放Span元数据
    } else {
        // 释放失败,记录错误日志(如内存被其他进程占用)
        perror("munmap failed");
    }
}
3.1.2 延迟释放策略

为避免频繁调用munmap,PageCache采用​​延迟释放​​策略:

  • ​空闲页缓存​​:空闲页不会立即归还给操作系统,而是保留一段时间(如5分钟);
  • ​定时清理​​:通过后台线程定期扫描空闲页列表,释放长期未使用的页。

​代码示例(定时清理)​​:

代码语言:javascript
复制
// 定时清理空闲页的线程函数
void* page_cache_cleanup_thread(void* arg) {
    PageCache* cache = (PageCache*)arg;
    while (1) {
        sleep(300);  // 每5分钟清理一次
        pthread_mutex_lock(&cache->lock);
        
        // 遍历空闲页列表,释放超过阈值的页
        Span* current = cache->free_spans;
        while (current) {
            if (current->page_count >= PAGE_RELEASE_THRESHOLD) {
                // 从空闲链表中移除
                remove_span_from_free_list(cache, current);
                
                // 释放系统页
                page_cache_recycle_span(cache, current);
            }
            current = current->next;
        }
        
        pthread_mutex_unlock(&cache->lock);
    }
    return NULL;
}
3.2 关键设计:延迟释放与系统调用优化
  • ​延迟释放​​:减少munmap调用次数,降低系统开销;
  • ​页级对齐​​:回收的内存块按页对齐,避免碎片化;
  • ​定时清理​​:平衡内存利用率和系统调用频率。

四、三级协同回收:从对象到系统的完整链路

以释放一个2KB对象为例,三级协同回收的完整流程如下:

  1. ​ThreadCache本地回收​​: 线程调用thread_cache_free释放对象,对象被插入本地空闲链表(哈希桶索引为8)。若本地链表已满(如16个对象),触发批量回收,将多余对象转移到CentralCache。
  2. ​CentralCache全局回收​​: CentralCache接收到对象后,通过ObjectToSpanMap找到对应的Span,将其标记为空闲。检查相邻Span,若空闲则合并为更大的Span(如从2KB合并为4KB)。若合并后的Span超过页大小阈值(如4KB),触发分级回收,将Span回收到PageCache。
  3. ​PageCache系统级回收​​: PageCache接收到Span后,计算其对应的系统页范围(如4KB对应1页),调用munmap释放内存。若空闲页过多,触发定时清理线程,释放长期未使用的页。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-06-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 一、ThreadCache回收:线程本地的「无锁快速回收」
    • 1.1 回收流程:本地链表直接操作
      • 1.1.1 对象回收到空闲链表
      • 1.1.2 本地缓存满时的批量回收
    • 1.2 关键设计:无锁与本地化
  • 二、CentralCache回收:全局共享的「Span合并与分级回收」
    • 2.1 回收流程:从对象到Span的聚合
      • 2.1.1 对象映射到Span
      • 2.1.2 空闲Span的合并与分级回收
    • 2.2 关键设计:Span合并与分级策略
  • 三、PageCache回收:系统级的「内存归还与延迟释放」
    • 3.1 回收流程:从Span到系统页的释放
      • 3.1.1 Span到系统页的转换
      • 3.1.2 延迟释放策略
    • 3.2 关键设计:延迟释放与系统调用优化
  • 四、三级协同回收:从对象到系统的完整链路
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档