内存分配中的 Arena 概念


什么是 Arena?

Arena(竞技场/分配区)是现代内存分配器(如 glibc malloc、jemalloc)中采用的一种核心架构,用于解决多线程环境下的内存分配性能问题。它的本质是将全局的堆内存分割成多个相对独立的区域,每个线程优先从自己的区域分配内存,从而减少锁竞争


为什么需要 Arena?

单线程时代的问题

// 传统单堆内存管理
static char heap[HEAP_SIZE];
static char* current = heap;

void* malloc_single(size_t size) {
    void* ptr = current;
    current += size;
    return ptr;
}

多线程时代的挑战

// 多线程竞争全局堆需要加锁
std::mutex heap_mutex;
void* heap_base;

void* malloc_global_lock(size_t size) {
    std::lock_guard<std::mutex> lock(heap_mutex);  // 性能瓶颈!
    // ... 分配逻辑
    return ptr;
}

glibc malloc 的 Arena 实现

1. Arena 数据结构

// glibc malloc 的 arena 结构(简化)
struct malloc_state {
    mutex_t mutex;                    // 本arena的锁
    mchunkptr top;                    // 指向top chunk
    mchunkptr bins[NBINS * 2 - 2];    // 各种大小的bin链表
    unsigned int binmap[BINMAPSIZE];  // bin位图
    struct malloc_state* next;        // 链表下一个arena
    INTERNAL_SIZE_T system_mem;       // 本arena从系统分配的内存总量
    INTERNAL_SIZE_T max_system_mem;
};

2. Arena 分配策略

// glibc 的 arena 管理逻辑
static struct malloc_state main_arena;  // 主arena
static struct malloc_state** free_list; // 空闲arena列表

// 线程获取arena的流程
struct malloc_state* arena_get(thread_id tid, size_t size) {
    struct malloc_state* arena;
    
    // 1. 首先尝试线程本地存储(TLS)中获取绑定的arena
    arena = get_thread_arena();
    if (arena != NULL && trylock_arena(arena)) {
        return arena;  // 成功获取自己的arena
    }
    
    // 2. 尝试从空闲arena列表获取
    arena = get_free_arena();
    if (arena != NULL) {
        set_thread_arena(arena);
        return arena;
    }
    
    // 3. 创建新的arena
    if (arena_count < max_arenas) {
        arena = create_new_arena();
        set_thread_arena(arena);
        return arena;
    }
    
    // 4. 所有方法失败,等待主arena
    lock_arena(&main_arena);
    return &main_arena;
}

3. Arena 数量限制

# glibc 的 arena 数量计算规则
number_of_arenas = min(
    # 核心数的倍数
    8 * number_of_cpu_cores,  
    # 或内存大小的函数
    total_memory / (128 * 1024),
    # 系统限制
    MAX_ARENAS
)

jemalloc 的 Arena 改进

1. 更细粒度的 Arena 管理

// jemalloc 的 arena 结构
struct arena_s {
    // 每个arena有自己的size classes
    size_class_t size_classes[SC_NSIZES];
    
    // 更精细的bin管理
    bin_t bins[NBINS];
    
    // 使用extent(范围)而不是chunk
    extent_tree_t extents_dirty;    // 脏extents
    extent_tree_t extents_muzzy;    // 模糊extents  
    extent_tree_t extents_retained; // 保留extents
};

2. Arena 分配算法对比

特性 glibc malloc jemalloc
Arena 数量 有限制(CPU核心数×8) 可配置,通常更多
内存回收 不积极,RSS保持高位 积极,通过purge线程
碎片管理 简单的bin链表 基于extent的range管理
线程绑定 线程与arena绑定 更灵活的分配策略

Arena 的生命周期问题

glibc 的 Arena 内存滞留

void worker_thread() {
    // 线程分配大量内存
    void* buffer = malloc(1024 * 1024);  // 1MB
    
    // 处理工作...
    process_data(buffer);
    
    free(buffer);  // 释放内存
    // 但arena仍然保留这些内存,不立即归还系统
}

// 问题:即使线程结束,arena及其内存仍然存在

jemalloc 的解决方案

// jemalloc 的arena清理机制
void arena_decay(arena_t* arena) {
    // 定期检查并清理空闲extents
    if (should_purge(arena)) {
        // 通过madvise释放物理内存
        extent_purge(extent);
    }
}

// 背景线程定期执行decay
void* jemalloc_bg_thread(void* arg) {
    while (true) {
        sleep(decay_interval);
        for_each_arena(arena_decay);
    }
}

实际影响示例

高并发场景下的内存使用

# 初始状态
PID    RSS     VSZ    COMMAND
1234   100M    500M   my_grpc_server

# 并发测试后 - glibc malloc
PID    RSS     VSZ    COMMAND  
1234   450M    500M   my_grpc_server  # RSS不下降!

# 并发测试后 - jemalloc
PID    RSS     VSZ    COMMAND
1234   120M    500M   my_grpc_server  # RSS正常回收

代码层面的观察

#include <malloc.h>
#include <iostream>

void print_arena_info() {
    struct mallinfo mi = mallinfo();
    std::cout << "Arena total: " << mi.arena << " bytes\n";
    std::cout << "Free chunks: " << mi.fordblks << " bytes\n";
    std::cout << "MMAP regions: " << mi.hblks << "\n";
}

// 在glibc中,即使fordblks很大,RSS也可能不下降

总结

Arena 的核心价值

  • 减少锁竞争:多线程并行分配,提高并发性能
  • 提高缓存局部性:线程使用自己的arena,缓存命中率更高
  • 隔离内存区域:不同arena相对独立,减少干扰

关键区别

  • glibc:arena管理相对保守,内存回收不积极,导致RSS居高不下
  • jemalloc:主动的内存回收和更精细的arena管理,内存使用更高效

这就是为什么在高并发gRPC服务中,切换到jemalloc能显著改善内存回收问题的根本原因。

posted @ 2025-10-19 10:06  guanyubo  阅读(18)  评论(0)    收藏  举报