一、前言
C++开发中频繁调用new/delete、malloc/free会带来两个严重问题:
-
系统调用开销大:每次new会进入内核堆管理器,频繁分配释放CPU开销高
-
产生大量外部内存碎片:小块内存反复申请释放,空闲内存被切割成大量不连续小块,明明总空闲内存充足,却无法分配大内存
在长期运行的网络服务(如前面手写HTTP服务器),大量短生命周期对象反复创建销毁,碎片问题会持续恶化。
池化技术就是提前一次性申请一大块内存,程序自己管理内存分配释放,减少系统调用,抑制内存碎片。
二、内部碎片与外部碎片
-
外部碎片:空闲内存不连续,分散在各处,总和足够,但无法满足大块连续内存申请。频繁malloc/new小块内存极易产生。
-
内部碎片:分配的内存块比实际需要的大,块内部多出的空间浪费。对齐、内存池按固定块分配会带来内部碎片。
权衡:对象池会牺牲一部分内部碎片,换取消除外部碎片,减少系统调用开销。长期运行服务收益巨大。
三、内存池 vs 对象池
|----------------|-------------------------------------|-----------------------------|
| 类型 | 特点 | 适用场景 |
| 内存池 MemoryPool | 管理一大块原始内存,支持分配不同大小内存块;偏底层内存管理 | 大小不固定的内存申请,通用内存管理 |
| 对象池 ObjectPool | 只管理同一类型对象,固定对象大小;复用对象,不反复调用构造析构 | 大量频繁创建销毁的同类型对象,网络连接对象、消息结构体 |
对象池属于内存池的一种特化场景。对象池不仅复用内存,还可以复用对象,避免反复构造、析构函数开销。
四、STL allocator分配器原理
STL容器默认使用std::allocator,底层就是封装malloc/free。
cpp
template<class T>
class allocator
{
public:
T* allocate(size_t n)
{
return (T*)malloc(n*sizeof(T));
}
void deallocate(T* p, size_t n)
{
free(p);
}
// construct调用placement‑new,destroy调用析构
};
关键两个概念:
construct / destroy:placement new定位new,在已分配好的内存上执行构造函数;手动调用析构函数,不释放内存
自定义分配器就是重写allocate/deallocate,把内存来源从malloc换成自己的内存池。
五、简易固定大小对象池完整实现
对象池核心逻辑:
-
预先申请一批T对象内存,放入空闲链表
-
获取对象:从空闲链表取一块内存,placement new执行构造
-
归还对象:调用析构,内存放回空闲链表,不释放给操作系统
-
销毁池的时候统一释放整块内存
cpp
#include <iostream>
#include <vector>
#include <cstddef>
#include <memory>
using namespace std;
template<typename T>
class ObjectPool
{
private:
struct Block
{
Block* next;
char data[sizeof(T)];
};
Block* m_freeList{nullptr};
vector<Block*> m_memoryChunks;
size_t m_chunkSize;
void allocChunk()
{
Block* chunk = new Block[m_chunkSize];
m_memoryChunks.push_back(chunk);
for(size_t i = 0; i < m_chunkSize; ++i)
{
chunk[i].next = m_freeList;
m_freeList = &chunk[i];
}
}
public:
explicit ObjectPool(size_t chunkSize = 1024) : m_chunkSize(chunkSize)
{
allocChunk();
}
// 获取对象
template<typename... Args>
T* acquire(Args&&... args)
{
if(!m_freeList)
{
allocChunk();
}
Block* blk = m_freeList;
m_freeList = blk‑>next;
T* obj = new (blk‑>data) T(forward<Args>(args)...);
return obj;
}
// 归还对象,不释放内存
void release(T* obj)
{
if(!obj) return;
obj‑>~T();
Block* blk = reinterpret_cast<Block*>(reinterpret_cast<char*>(obj) ‑ offsetof(Block, data));
blk‑>next = m_freeList;
m_freeList = blk;
}
~ObjectPool()
{
for(auto p : m_memoryChunks)
{
delete[] p;
}
}
// 禁止拷贝
ObjectPool(const ObjectPool&) = delete;
ObjectPool& operator=(const ObjectPool&) = delete;
};
// 测试对象
struct Msg
{
int id;
char buf[256];
Msg(int i):id(i){}
};
int main()
{
ObjectPool<Msg> pool(512);
Msg* p1 = pool.acquire(1001);
Msg* p2 = pool.acquire(1002);
cout << p1‑>id << endl;
pool.release(p1);
Msg* p3 = pool.acquire(2003); // 复用p1的内存块
pool.release(p2);
pool.release(p3);
return 0;
}
关键点:offsetof计算结构体偏移;placement new在已有内存上构造对象;归还时只调用析构,delete\[\]只在池析构时执行。
六、简易内存池(固定块内存池)思路
内存池面向任意大小内存的简化思路:
-
一次性向系统申请一大块连续内存,作为内存池内存区
-
维护空闲链表,记录空闲块地址、大小
-
分配:遍历空闲链表,找到满足大小的块;分割块,剩余部分放回空闲链表
-
释放:把归还的块放回空闲链表,尝试和前后相邻空闲块合并,减少外部碎片
完整工业级内存池实现复杂,需要处理对齐、越界检测、碎片合并;开源项目jemalloc/tcmalloc就是成熟的内存池实现,替换系统malloc。
七、性能对比测试
cpp
#include <chrono>
int main()
{
const int LOOP = 1000000;
// new delete
auto t1 = chrono::high_resolution_clock::now();
for(int i = 0; i < LOOP; i++)
{
Msg* m = new Msg(i);
delete m;
}
auto t2 = chrono::high_resolution_clock::now();
cout <<"new/delete cost:"<<chrono::duration_cast<chrono::microseconds>(t2‑t1).count()<<"us"<<endl;
// 对象池
ObjectPool<Msg> pool(1024);
auto t3 = chrono::high_resolution_clock::now();
for(int i = 0; i < LOOP; i++)
{
Msg* m = pool.acquire(i);
pool.release(m);
}
auto t4 = chrono::high_resolution_clock::now();
cout <<"object pool cost:"<<chrono::duration_cast<chrono::microseconds>(t4‑t3).count()<<"us"<<endl;
return 0;
}
现象:百万次循环,对象池速度会显著快于原生new/delete;循环次数越大,差距越明显。
allocate/deallocate:只负责内存的申请释放,不碰对象构造析构
八、工程什么时候选择池化技术
✅适合使用对象池:
-
对象大小固定,频繁创建销毁,生命周期短(网络消息、连接上下文)
-
长期运行后台服务,需要抑制内存碎片
-
new/delete成为性能热点,perf观测堆分配占比高
❌不适合:
-
对象大小差异巨大,会造成严重内部碎片
-
对象数量波动极小,几乎不频繁创建销毁
-
简单短时程序,程序退出直接回收全部内存,碎片不影响
九、池化技术高频坑点
-
内存泄漏风险:acquire拿到对象忘记release,内存永远不会还给系统
-
野对象复用:release之后外部继续使用对象指针,野指针问题
-
对齐问题内存块没有按照CPU对齐,访问异常崩溃
-
多线程安全上面示例是单线程版本;多线程环境acquire/release需要加锁
-
析构遗漏归还对象忘记调用~T(),对象资源没有释放(文件句柄、其他堆内存)
-
池对象生命周期早于对象池已经析构,外部还持有池产出的指针,程序崩溃
十、面试真题解析
Q1 内存碎片分为哪两种?怎么产生,如何缓解?
外部碎片:空闲内存不连续,小块反复分配释放产生;用内存池、对象池缓解。内部碎片:分配块大于实际需要,对齐、固定块池带来的浪费。
Q2 对象池和内存池区别?
对象池面向固定类型对象,复用对象,减少构造析构;内存池面向原始内存块,可以分配不同大小内存。对象池是内存池的特例。
Q3 STL allocator的allocate和construct区别?
allocate只申请内存,不构造对象;construct调用placement new在已分配内存执行构造函数;deallocate释放内存,destroy调用析构。
Q4 对象池为什么要用offsetof?
拿到T*对象指针,要反向找到外层Block块的起始地址,通过data成员的偏移量回退,拿到Block*,放回空闲链表。
Q5 线上服务除了自己写内存池还有什么方案?
替换系统malloc,使用jemalloc/tcmalloc,库内部自带内存池,减少碎片,提升多线程堆分配性能,很多开源服务Nginx、Redis都使用。
十一、今日总结
掌握内存池、对象池核心:
✅ new/malloc带来的性能开销、内外存碎片原理
✅ 内存池与对象池概念区分、适用场景
✅ STL allocator底层allocate/deallocate/construct/destroy原理
✅ 完整可运行对象池模板实现,offsetof、placement‑new关键点
✅ 性能测试对比代码,理解池化带来的性能收益
✅ 工程选型标准、池化技术各类坑点
✅ 面试高频问题梳理