9、内存池初始化与销毁:预分配策略、按需增长、内存池的销毁与资源回收

内存池这东西,说白了就是一块你提前圈好的地。你想想看,每次要内存都去系统调用 malloc,那开销可不小。我早年做嵌入式项目时,就吃过这个亏——频繁分配小对象,性能直接崩了。后来自己写了个内存池,世界清净了。

今天咱们就聊聊内存池的初始化与销毁。这两个操作看似简单,但坑不少。我踩过的坑,今天一并告诉你。

9.1 预分配策略:一次申请,多次使用

预分配,就是内存池在创建时,一次性向操作系统申请一大块连续内存。之后所有小对象分配,都在这块内存里切分。

我个人习惯的做法是:先估算峰值使用量,再乘以一个安全系数(比如1.5倍)。为什么?因为内存池一旦创建,再扩容就比较麻烦。你总不希望运行到一半,内存不够用吧?

核心原则:预分配的大小 = 预估峰值 × 安全系数。安全系数建议1.2~2.0,视业务场景而定。

来看一个简单的预分配实现:

// 内存池结构体
typedef struct mempool_s {
    void   *pool_start;   // 池起始地址
    size_t  pool_size;    // 池总大小
    void   *free_ptr;     // 当前空闲位置指针
    size_t  free_size;    // 剩余空闲大小
} mempool_t;

// 预分配初始化
mempool_t* mempool_create(size_t size) {
    mempool_t *mp = (mempool_t*)malloc(sizeof(mempool_t));
    if (!mp) return NULL;

    mp->pool_start = malloc(size);
    if (!mp->pool_start) {
        free(mp);
        return NULL;
    }

    mp->pool_size = size;
    mp->free_ptr  = mp->pool_start;
    mp->free_size = size;

    return mp;
}

嗯,这里要注意:free_ptrfree_size 是分配器的核心状态。每次分配时,我们只需要移动指针、减少剩余大小即可。这比 malloc 快得多——没有链表遍历,没有系统调用。

9.2 按需增长:内存不够了怎么办?

预分配再好,也架不住业务突发。我在一个网络服务器项目中就遇到过:平时内存池够用,但某次流量洪峰,内存池直接爆了。程序崩溃,线上事故。

从那以后,我设计的内存池都支持按需增长。说白了就是:当池内内存不够时,自动再申请一块新的内存块,挂到池的链表上。

我的经验:按需增长时,新块的大小建议是当前池大小的1.5倍或2倍。太小了频繁扩容,太大了浪费内存。我一般用2倍,简单粗暴。

来看扩容的实现思路:

// 内存块节点(用于链表管理)
typedef struct memblock_s {
    struct memblock_s *next;
    void   *start;
    size_t  size;
    void   *free_ptr;
    size_t  free_size;
} memblock_t;

// 内存池(支持增长)
typedef struct mempool_grow_s {
    memblock_t *head;      // 第一个块
    memblock_t *current;   // 当前正在使用的块
    size_t      block_size;// 默认块大小
} mempool_grow_t;

// 扩容:追加一个新块
int mempool_grow(mempool_grow_t *mp) {
    size_t new_size = mp->block_size * 2;  // 每次翻倍
    memblock_t *block = (memblock_t*)malloc(sizeof(memblock_t));
    if (!block) return -1;

    block->start = malloc(new_size);
    if (!block->start) {
        free(block);
        return -1;
    }

    block->size     = new_size;
    block->free_ptr = block->start;
    block->free_size = new_size;
    block->next     = NULL;

    // 挂到链表尾部
    mp->current->next = block;
    mp->current = block;
    mp->block_size = new_size;  // 更新默认大小

    return 0;
}

为什么翻倍?因为这样扩容次数是 O(log N),而不是 O(N)。你想想看,如果每次只加固定大小,扩容100次后,链表上挂了一堆小碎片,分配效率直线下降。

注意:按需增长虽然解决了内存不够的问题,但也会带来碎片。每个块内部是连续的,但块与块之间不连续。所以大对象分配时,可能跨块失败。我一般建议:大对象走单独的内存池,或者直接用 malloc。

9.3 内存池的销毁:资源回收的艺术

销毁内存池,听起来简单——free 掉不就完了?但实际项目中,坑多着呢。

我曾经在一个多线程项目中,销毁内存池时忘了加锁,结果另一个线程还在分配内存,直接 double free,程序崩溃。排查了一整天,最后发现是销毁顺序的问题。

销毁的核心原则就一条:先停止使用,再释放资源

来看销毁的实现:

// 销毁内存池(单块版本)
void mempool_destroy(mempool_t *mp) {
    if (!mp) return;

    if (mp->pool_start) {
        free(mp->pool_start);
        mp->pool_start = NULL;
    }

    free(mp);
}

// 销毁内存池(多块版本,支持增长)
void mempool_grow_destroy(mempool_grow_t *mp) {
    if (!mp) return;

    memblock_t *block = mp->head;
    while (block) {
        memblock_t *next = block->next;
        if (block->start) {
            free(block->start);
            block->start = NULL;
        }
        free(block);
        block = next;
    }

    free(mp);
}

嗯,这里有个细节:销毁时一定要把指针置 NULL。为什么?因为如果代码中有 bug,销毁后还有人访问内存池,置 NULL 后至少能快速崩溃,而不是产生难以排查的野指针问题。

避坑指南:我曾经在销毁后忘记置 NULL,结果另一个模块的定时器回调还在使用内存池。数据写到了已释放的内存上,导致另一个完全不相关的对象被篡改。这种 bug 极难复现,排查了整整三天。从那以后,我销毁任何资源后,都会把指针置 NULL。

9.4 资源回收的完整流程

一个完整的内存池生命周期,应该是这样的:

  1. 创建:预分配一块或多块内存
  2. 使用:从池中分配、释放对象
  3. 扩容(可选):按需追加新块
  4. 销毁:释放所有内存块,清理状态

我习惯在销毁函数中加一个断言检查:如果池中还有未释放的对象,就打印警告。这能帮你发现内存泄漏。

// 带检查的销毁
void mempool_safe_destroy(mempool_t *mp) {
    if (!mp) return;

    // 检查是否有未释放的对象
    size_t used = mp->pool_size - mp->free_size;
    if (used > 0) {
        fprintf(stderr, "WARNING: mempool still has %zu bytes allocated!\n", used);
        // 这里可以记录调用栈,方便排查
    }

    free(mp->pool_start);
    mp->pool_start = NULL;
    mp->free_ptr   = NULL;
    mp->pool_size  = 0;
    mp->free_size  = 0;

    free(mp);
}

小技巧:在调试版本中,我还会在销毁前遍历所有已分配对象,检查是否有内存越界。具体做法是:在每个对象前后加哨兵字节(比如 0xDEADBEEF),销毁时检查哨兵是否被改写。如果被改写了,说明有人写越界了。

9.5 知识体系总览

下面这张图,把内存池初始化与销毁的核心逻辑串起来了。你可以看到:预分配是起点,按需增长是弹性机制,销毁是终点。三者缺一不可。

内存池初始化与销毁核心流程 预分配策略 按需增长 销毁与回收 一次性申请大块连续内存 估算峰值 × 安全系数 指针 + 剩余大小 管理分配 链表管理多个内存块 新块大小 = 当前块 × 2 扩容次数 O(log N) 先停止使用,再释放资源 遍历链表释放所有块 指针置 NULL,防止野指针 核心目标:高性能分配 + 零泄漏 + 快速回收

这张图里,三个主流程是串行的:先预分配,不够了就按需增长,最后统一销毁。每个流程下都有具体的实现要点。我个人建议你把这个图打印出来贴在工位上,写代码时对照着看,不容易漏掉关键步骤。

好了,内存池的初始化与销毁就聊到这儿。记住:预分配要留余量,按需增长要翻倍,销毁时要置 NULL。这三个习惯,能帮你省下不少调试时间。


公众号:蓝海资料掘金营,微信 deep3321