内存碎片化是 C++ 在高并发服务中的一个关键性能顽疾。碎片分成两类:外部碎片——空闲的内存块零散、不连续,总的空闲内存充足,却无法满足大块内存的分配;内部碎片——分配出去的内存大于实际需求,剩下的空间被闲置。
高并发场景下,高频次的小内存分配释放、多线程争抢内存,会使内存利用率不断降低,进而引发系统频繁调用 brk/mmap、服务 OOM、接口 RT 抖动等严重状况。
C++ 高并发场景下解决内存碎片化的关键思路:降低系统级的内存调用、统一内存的分配粒度、重复使用内存块、避免多线程争抢、精准把控内存的生命周期。核心是舍弃标准库默认的全面堆分配逻辑,运用分层、分级的管理办法。
高并发下,内存碎片化是什么?
C++ 编程中,高并发指很多线程一起运行、争抢资源。比如网络服务器同时接入大量客户端连接,每个连接都可能触发读写操作,要求快速响应;再比如金融交易系统,大量订单并发处理,要求在高并发下稳定运行。
高并发情境下,内存碎片化就像整理书架:起初书架规整,书一本本紧密排列。但不断地从不同位置抽走书(释放内存)、再插入新书(分配内存)之后,书架上会慢慢出现许多小空隙(空闲内存块),它们零散分布。这时再想放一本大厚书(申请大块连续内存),就很难找到放得下的连续空间——这就是内存碎片化。
在 C++ 中,内存碎片化指系统里存在大量不连续的小块空闲内存,无法满足大块内存的请求。
还不理解内存碎片化,可以参考这篇《告别内存碎片,从理解内存池开始》。
内存碎片的产生原因
(1)频繁的动态内存分配释放
高并发场景下,线程不断创建、销毁对象,意味着频繁的内存分配与释放。比如并发网络系统里,每新建一个连接就要分配内存存储连接信息,连接断开时又释放。这个过程非常频繁,就像前面的书架,内存很快就会碎片化。
(2)分配释放顺序不匹配
不同线程的内存使用模式不同,分配和释放的顺序也不一致。例如线程 A 先分配一大块内存,线程 B 在其中分配了几个小块,随后 B 释放其中一些——释放顺序与分配顺序不一致,就会在大块中留下许多小空闲区域,这些区域很难被其他线程再次利用,从而形成碎片。
(3)分配算法缺陷
部分内存分配算法在高并发下表现不佳。例如常见的首次适配(First Fit)算法,从内存起始位置找第一个放得下的空闲块。高并发下内存频繁分配释放,空闲块分布趋于零散,首次适配要花更多精力寻找合适的块,还容易把大块空隙分割成小块空闲,进一步加剧碎片化。
内存碎片化的负面影响
- 性能下降:碎片化后,分配器为找到足够的空闲块,要花费更多时间遍历内存空间。比如游戏服务器频繁给新玩家分配内存,碎片严重时每次分配耗时增加,帧率下降,玩家感觉卡顿。
- 内存分配失败:碎片导致没有连续大块内存。程序申请大块内存时,即使总空闲内存足够,也会因为小块分散存在而无法满足要求,导致分配失败。比如图像处理程序加载高分辨率图片需要大块内存,碎片严重时可能分配失败、图像加载出错。
- 内存利用率降低:大量小块空闲内存无法被充分利用,造成浪费。就像仓库看上去还有不少空间,但都是狭小的角落,放不下大型货物,实际利用率很低。
解决内存碎片化,有哪些方法?
(1)减少动态内存操作
在 C++ 中,动态内存分配(new/delete)是导致碎片化的主要原因之一,减少动态内存操作是关键办法。优先使用栈内存或静态内存——它们的分配释放由编译器自动管理,不产生碎片:
void function() {
int stackVariable = 10; // 使用栈内存
static int staticVariable = 20; // 使用静态内存
// ...
}
stackVariable 是栈变量,函数执行时分配,函数结束后自动释放;staticVariable 是静态变量,程序启动时分配,程序结束后释放。它们都不像动态内存分配那样造成碎片。
必须使用动态内存时,尽量降低 new 和 delete 的使用次数,考虑对象复用,不要重复创建销毁对象。比如游戏开发中,子弹、怪物这类高频使用的对象,可以预先创建一批放进对象池,需要时从池里获取,用完放回,而不是每次都创建销毁新对象。
(2)内存池技术
内存池是常用的解决碎片化的技术。基本思路:程序启动时预先分配大块内存,运行过程中从这块内存里分配、释放小块,而不是直接向操作系统申请释放。这样避免了频繁的系统内存申请释放,降低碎片产生的概率。
比如服务器频繁分配释放内存存储网络数据包,用内存池能明显提高分配释放效率。一个简易的固定尺寸内存池:
class MemoryPool {
private:
struct Block {
Block* next;
};
Block* freeList;
char* memory;
size_t blockSize;
size_t poolSize;
public:
MemoryPool(size_t count, size_t size)
: blockSize((size + alignof(Block) - 1) / alignof(Block) * alignof(Block)),
poolSize(count) {
memory = new char[blockSize * count];
freeList = nullptr;
// 构建空闲链表
for (int i = count - 1; i >= 0; --i) {
Block* block = reinterpret_cast<Block*>(memory + i * blockSize);
block->next = freeList;
freeList = block;
}
}
~MemoryPool() {
delete[] memory;
}
void* allocate() {
if (!freeList) return nullptr;
Block* block = freeList;
freeList = freeList->next;
return block;
}
void deallocate(void* ptr) {
if (ptr) {
Block* block = static_cast<Block*>(ptr);
block->next = freeList;
freeList = block;
}
}
};
内存池适用于频繁分配、释放相同尺寸内存块的场景,例如网络数据包处理、游戏对象管理等。这类场景下使用内存池能极大提升内存使用效率,降低碎片。
(3)智能指针
智能指针是 C++ 标准库提供的自动管理动态内存的工具。它通过 RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制,在对象生命周期结束时自动释放所管理的内存,避免手动释放带来的内存泄漏和碎片化。
标准库提供三种主要智能指针:std::unique_ptr 独占式管理动态内存,不允许拷贝但可以移动,性能开销小;std::shared_ptr 共享式管理,通过引用计数跟踪有多少指针指向同一对象,计数归零时自动释放;std::weak_ptr 是弱引用,不增加引用计数,主要用于解决 shared_ptr 之间的循环引用问题。
#include <memory>
#include <iostream>
int main() {
// 使用 std::unique_ptr
std::unique_ptr<int> uniquePtr = std::make_unique<int>(10);
// 使用 std::shared_ptr
std::shared_ptr<int> sharedPtr1 = std::make_shared<int>(20);
std::shared_ptr<int> sharedPtr2 = sharedPtr1; // 引用计数增加
// 使用 std::weak_ptr
std::shared_ptr<int> shared = std::make_shared<int>(30);
std::weak_ptr<int> weak = shared;
if (std::shared_ptr<int> locked = weak.lock()) {
std::cout << "Weak pointer locked, value: " << *locked << std::endl;
} else {
std::cout << "Weak pointer expired" << std::endl;
}
return 0;
}
依靠智能指针的自动内存管理机制,能有效降低内存碎片化的概率。
(4)打造专属分配器
自定义内存分配器根据程序的特定需求设计实现,能依据程序的内存使用模式采用更高效的分配算法,降低碎片产生概率。比如数据库管理系统的数据存取模式固定,用自定义分配器针对性优化,可以提高内存使用效率。常见的算法有伙伴系统、slab 分配器等:
- 伙伴系统:把内存按不同大小划分成组,分配时从合适的组里找空闲块,没有合适的大块就拆分更大的块;释放时检查相邻内存块是否空闲,空闲则合并成更大的块。
- Slab 分配器:把内存划分为若干 slab,每个 slab 包含多个大小相同的对象,分配时直接从对应 slab 获取,释放后放回原 slab,降低碎片产生的概率。
一个简易的自定义内存分配器示例:
class CustomAllocator {
private:
char* memoryPool; // 内存池
size_t poolSize; // 内存池大小
void** freeList; // 空闲链表
public:
CustomAllocator(size_t size) : poolSize(size) {
memoryPool = new char[poolSize];
freeList = reinterpret_cast<void**>(memoryPool);
// 初始化空闲链表
for (size_t i = 0; i < poolSize / sizeof(void*); ++i) {
freeList[i] = reinterpret_cast<void*>(
reinterpret_cast<char*>(memoryPool) + (i + 1) * sizeof(void*));
}
freeList[poolSize / sizeof(void*) - 1] = nullptr;
}
~CustomAllocator() {
delete[] memoryPool;
}
void* allocate(size_t size) {
if (freeList == nullptr) return nullptr;
void* ptr = freeList[0];
freeList = reinterpret_cast<void**>(ptr);
return ptr;
}
void deallocate(void* ptr) {
reinterpret_cast<void**>(ptr)[0] = freeList;
freeList = reinterpret_cast<void**>(ptr);
}
};
CustomAllocator 通过维护空闲链表完成内存的分配与释放,降低碎片出现的概率。
(5)定期整理内存
部分内存分配器具备内存整理功能,把分散的空闲内存块整合成更大的连续块,减少碎片数量。整理方式是遍历内存空间,把相邻的空闲块合并成更大的空闲块。
长时间运行的程序,随着内存不断分配释放,碎片会逐步增多,定期整理能有效缓解。以 jemalloc 为例,它提供整理函数:
mallctl("thread.tcache.flush", NULL, NULL, NULL, 0);
可以强制把线程缓存里的内存块归还到整个内存池,实现整理。但整理操作通常有一定性能开销,因为需要遍历内存并执行合并。所以整理方式与频率要综合权衡:频繁使用、性能要求高的程序不宜太频繁整理;内存使用稳定、对利用率要求高的程序可以适当增加整理频次。
(6)高效分配器推荐
除了自定义分配器,还可以直接选用针对高并发优化的高效分配器,如 jemalloc、tcmalloc:
- jemalloc:Facebook 开发的高效分配器,碎片管理能力出色,采用多 arena 架构,每个线程使用独立的内存区域,减少锁竞争。高并发场景下能有效减少碎片、提高分配效率。
- tcmalloc(Thread-Caching Malloc):Google 开发,Chrome 浏览器等大型项目广泛使用。每个线程维护本地缓存,减少锁竞争,小对象分配开销极低,适合多线程服务器和大量小对象分配的场景。
使用这些分配器通常只需编译时链接对应的库即可:jemalloc 在编译命令中添加 -ljemalloc,tcmalloc 添加 -ltcmalloc。根据程序特点和需求选择合适的高效分配器,可以提高内存使用效率、减少碎片化。
如何选择
实际应用中需要综合考虑程序的具体情况和需求:对象创建销毁非常频繁且大小固定,内存池是不错的选择;动态内存分配多、对内存泄漏敏感,智能指针能有效解决;对分配性能要求极高、有特定内存使用模式,自定义分配器或 jemalloc/tcmalloc 这类高效分配器更合适。