项目源码:https://gitee.com/kkkred/thread-caching-malloc
在高并发场景中,内存分配与回收的效率直接影响系统吞吐量。传统单内存池因全局锁和碎片问题,难以满足高性能需求。而分层内存池框架(ThreadCache、CentralCache、PageCache)通过「线程本地回收+全局协调+系统级释放」的三级回收机制,实现了高效的内存管理。本文将深入解析这三层组件的回收逻辑、关键设计点及协作流程,揭示高并发场景下内存回收的「无锁化」与「碎片控制」之道。
ThreadCache作为线程私有的内存池,负责高频小对象(≤1KB)的回收。其核心目标是无锁、高效,避免多线程竞争导致的性能损耗。
ThreadCache的回收操作完全在本地线程完成,无需全局锁。其核心步骤如下:
当线程调用thread_cache_free释放对象时,首先根据对象大小计算哈希桶索引(对齐后的Size Class),然后将对象指针插入对应空闲链表的头部。
代码示例:
// 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; // 更新链表头指针
}ThreadCache的空闲链表容量有限(如每个Size Class最多缓存16个对象)。当本地缓存满时,需将多余对象批量回收到CentralCache,避免内存浪费。
触发条件:
MAX_LOCAL_CACHE_SIZE(如16);回收逻辑:
// 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(¢ral_cache.lock);
// 将本地空闲链表中的对象转移到CentralCache的空闲链表
void* obj = *bucket;
while (obj) {
void* next = *(void**)obj;
central_cache_add_free_object(¢ral_cache, obj, i);
obj = next;
}
*bucket = NULL; // 清空本地链表
pthread_mutex_unlock(¢ral_cache.lock);
}
}CentralCache负责中大对象(>1KB)的回收,通过Span结构和双链表实现高效的内存块管理。其核心挑战是碎片控制和跨线程协调。
当ThreadCache或应用程序调用central_cache_free释放对象时,CentralCache需将对象转换为Span,并合并相邻的空闲Span以减少碎片。
每个对象在分配时会被关联到一个Span(记录其所属的内存块)。释放时,通过对象地址找到对应的Span,并将其标记为空闲。
数据结构支持:
代码示例:
// 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(¢ral_cache.map, obj_addr);
return entry ? entry->span : NULL;
}释放对象后,CentralCache会检查相邻的Span是否空闲。若相邻,则合并为更大的Span(减少碎片);若合并后的Span足够大(如超过页大小的2倍),则将其回收到PageCache。
合并逻辑:
// 合并相邻的空闲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);
}
}PageCache是内存池与操作系统的桥梁,负责大内存块(页级及以上)的回收。其核心目标是减少系统调用和优化内存利用率。
当CentralCache需要回收大内存块时,会调用page_cache_recycle_span将Span转换为系统页,并归还给操作系统。
Span记录了内存块的起始页号和总页数。PageCache根据这些信息,计算对应的虚拟地址范围,并调用munmap释放内存。
代码示例:
// 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");
}
}为避免频繁调用munmap,PageCache采用延迟释放策略:
代码示例(定时清理):
// 定时清理空闲页的线程函数
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;
}munmap调用次数,降低系统开销;以释放一个2KB对象为例,三级协同回收的完整流程如下:
thread_cache_free释放对象,对象被插入本地空闲链表(哈希桶索引为8)。若本地链表已满(如16个对象),触发批量回收,将多余对象转移到CentralCache。
ObjectToSpanMap找到对应的Span,将其标记为空闲。检查相邻Span,若空闲则合并为更大的Span(如从2KB合并为4KB)。若合并后的Span超过页大小阈值(如4KB),触发分级回收,将Span回收到PageCache。
munmap释放内存。若空闲页过多,触发定时清理线程,释放长期未使用的页。