内存池学习笔记
引言
在 C/C++ 程序中,频繁使用new/delete或malloc/free动态分配小块内存,会导致两个问题:一是调用系统调用(或运行时库)的开销大;二是产生大量内存碎片,降低性能和内存利用率。内存池(Memory Pool)预先申请一大块内存,然后由程序自己管理分配和回收,像从池子里取水一样快速获取小块内存。
如果把系统默认的分配器比作“每次打水都要去河里舀”,那么内存池就是“在家门口挖一口井,随用随取”—— 避免了远距离奔波和碎片化混乱。
前置知识
内存碎片:外部碎片(空闲块之间穿插着已分配块,导致无法合并成大块)和内部碎片(分配块比实际需求大)。
分配器(Allocator):管理内存申请与释放的系统组件。
对象池(Object Pool):一种特定内存池,专门分配固定大小的对象。
块(Block):内存池管理的基本单元。
链表:常用空闲块链表来管理未分配的内存。
核心思想
内存池的基本想法:
预先从系统申请一大块连续内存(称为pool)。
将这块内存按固定大小分割成多个小块(chunk),每个小块大小通常等于或稍大于应用程序请求的对象大小。
使用空闲链表(free list)将所有空闲块串起来。
分配时,直接从空闲链表中取出一块(O(1))。
释放时,将块插回空闲链表(O(1))。
固定大小内存池只适用于分配大小相同的对象。如果需要分配不同大小的对象,可以使用分级内存池(维护多个固定大小的池,如 8, 16, 32, … 字节)。
更高级的方案:slab 分配器(Linux 内核)、tcmalloc(Google)、jemalloc(Facebook)。
实现一个简单的固定大小内存池(C++)
cpp
#include <iostream> #include <cstdlib> #include <cassert> class MemoryPool { public: MemoryPool(size_t blockSize, size_t blockCount) : blockSize_(blockSize), blockCount_(blockCount) { // 分配一整块内存 pool_ = static_cast<char*>(std::malloc(blockSize * blockCount)); if (!pool_) throw std::bad_alloc(); // 初始化空闲链表:每个块的前 sizeof(FreeNode*) 字节存储下一个空闲块的地址 freeHead_ = reinterpret_cast<FreeNode*>(pool_); FreeNode* cur = freeHead_; for (size_t i = 1; i < blockCount; ++i) { char* nextBlock = pool_ + i * blockSize; cur->next = reinterpret_cast<FreeNode*>(nextBlock); cur = cur->next; } cur->next = nullptr; } ~MemoryPool() { std::free(pool_); } void* allocate() { if (!freeHead_) return nullptr; // 池已空 void* ptr = freeHead_; freeHead_ = freeHead_->next; return ptr; } void deallocate(void* ptr) { if (!ptr) return; // 将释放的块放回空闲链表头部 FreeNode* node = static_cast<FreeNode*>(ptr); node->next = freeHead_; freeHead_ = node; } // 禁止拷贝 MemoryPool(const MemoryPool&) = delete; MemoryPool& operator=(const MemoryPool&) = delete; private: struct FreeNode { FreeNode* next; }; char* pool_; FreeNode* freeHead_; size_t blockSize_; size_t blockCount_; }; // 使用示例 int main() { MemoryPool pool(sizeof(int), 100); int* a = (int*)pool.allocate(); *a = 42; std::cout << *a << std::endl; pool.deallocate(a); return 0; }要点:
每个空闲块的前几个字节存储指向下一个空闲块的指针(侵入式链表)。
分配和释放都是常数时间。
只适用于固定大小,且不保证线程安全。
性能与性质
| 特性 | 系统默认分配器 | 内存池 |
|---|---|---|
| 分配速度 | 较慢(涉及系统调用或复杂算法) | 极快(单链表操作) |
| 释放速度 | 较慢 | 极快 |
| 内存碎片 | 容易产生外部碎片 | 无外部碎片(但可能有内部碎片) |
| 适用场景 | 通用,大小不一 | 大量小块、大小固定或几种固定大小 |
| 线程安全 | 通常全局锁 | 需要额外实现 |
性质:
内存池不会将内存归还给操作系统(除非整个池销毁),适合长期运行且内存使用稳定的场景。
可以重载
new/delete操作符,使类对象自动使用内存池。对于多线程环境,可以给每个线程独立的本地池(thread-local pool)或使用原子操作保护空闲链表。
拓展
对象池(Object Pool):与内存池类似,但管理的是已经构造好的对象,分配时返回已存在的对象实例(如数据库连接池、线程池)。
池化技术:连接池、线程池、内存池都是“资源复用”思想的体现。
slab 分配器:Linux 内核的 slab 分配器将内存划分为不同大小的 cache,并维护每个对象的构造函数/析构函数。
伙伴系统(Buddy System):另一种内存管理算法,平衡碎片和分配速度。
总结
内存池通过预先申请大块内存并自行管理分配,避免了频繁的系统调用和内存碎片,极大提升了频繁分配/释放小块内存的性能。它是高性能服务器、游戏引擎、数据库等系统中的基本组件。掌握内存池的实现原理,不仅能写出更高效的代码,还能深入理解计算机内存管理的底层机制。
