new/delete频繁导致外部碎片,因其调用malloc/free在堆上留下大量不连续小空闲块,无法满足后续大内存请求;实时系统、长周期服务和嵌入式设备对此尤为敏感。

为什么 new / delete 频繁会导致内存碎片
堆分配器(如 glibc 的 malloc)在反复申请/释放不同大小的内存块后,容易留下大量无法被后续大块请求复用的小空闲区——这就是外部碎片。C++ 默认使用全局堆,new 和 delete 本质调用底层 malloc/free,不加干预时完全暴露这个问题。
关键不是“有没有碎片”,而是“碎片是否影响你的场景”:实时系统、长周期服务、嵌入式设备对碎片更敏感;短生命周期程序(如命令行工具)通常不用强干预。
- 小对象(
-
std::vector动态扩容、std::string多次append、临时std::shared_ptr创建都可能触发隐蔽的堆抖动 - 释放顺序和分配顺序不一致(比如后分配的先释放)会加剧空洞化,尤其在多线程下,不同线程的
free可能交错打乱内存布局
用 std::pmr::monotonic_buffer_resource 控制生命周期
这是 C++17 引入的最实用方案之一:它不真正归还内存给系统,而是按需增长 buffer,所有分配都在一个连续区域内推进,彻底避免外部碎片——代价是只能批量释放(buffer 整体重置)。
适合有明确作用域边界的场景:比如一帧渲染、一次 HTTP 请求处理、一个算法子阶段。
立即学习“C++免费学习笔记(深入)”;
#include <memory_resource>
#include <vector>
<p>void process_request() {
std::pmr::monotonic_buffer_resource pool{1024 * 1024}; // 1MB buffer
std::pmr::polymorphic_allocator<int> alloc{&pool};</p><pre class="brush:php;toolbar:false;">std::pmr::vector<int> v1{alloc};
v1.resize(10000);
std::pmr::vector<std::string> v2{alloc};
v2.emplace_back("hello");
// 所有内存随 pool 析构自动回收,无碎片}
- buffer 大小建议预估+留余量,过小会频繁 fallback 到上游 allocator(如
new_delete_resource()),反而引入碎片 - 不能用于需要单独
delete某个对象的场景——它只支持“全有或全无”的生命周期管理 - 注意线程安全:每个线程应持有独立
monotonic_buffer_resource实例,不要跨线程共享
自定义 operator new + 对象池(object_pool)管理固定大小对象
当某类对象(如 Event、Node)尺寸固定、数量可预期、生命周期较短,对象池是最直接的解法:预分配一大块内存,按固定 stride 切分,用 free-list 管理已释放节点。
比 monotonic_buffer_resource 更细粒度,支持单个对象回收,且不依赖 C++17。
template<typename T>
class object_pool {
std::vector<char> storage_;
std::vector<T*> free_list_;
public:
object_pool(size_t n) : storage_(n * sizeof(T)), free_list_(n) {
char* p = storage_.data();
for (size_t i = 0; i < n; ++i) {
free_list_[i] = new(p) T{}; // placement new
p += sizeof(T);
}
}
<pre class="brush:php;toolbar:false;">T* allocate() {
if (free_list_.empty()) return nullptr;
T* ptr = free_list_.back();
free_list_.pop_back();
return ptr;
}
void deallocate(T* ptr) {
new(ptr) T{}; // 调用析构+重置
free_list_.push_back(ptr);
}};
- 必须显式调用对象析构函数(如上例中的
new(ptr) T{}),否则资源泄漏 - 禁止混合使用:不能对
object_pool::allocate()返回的指针调用delete,也不能把普通new出来的对象传给deallocate - 如果对象含虚函数或非平凡析构,确保
T支持 trivial relocation,否则移动/拷贝可能出错
什么时候该放弃“零碎片”执念
过度优化碎片可能引入更大成本:锁竞争(并发池)、缓存局部性下降(大 buffer 跨页)、内存占用虚高(预分配未用空间)、维护复杂度上升(自定义分配器调试困难)。
真实项目中更值得优先做的三件事:
- 用
valgrind --tool=massif或gperftools的pprof确认碎片是否真是瓶颈,而不是臆测 - 优先减少不必要的动态分配:栈对象、
std::array替代小vector、std::string_view替代临时string - 对容器预分配容量:
vec.reserve(N)、unordered_map.reserve(N),避免多次 rehash 和内存搬移
碎片问题往往藏在“看起来无害”的间接分配里——比如第三方库的内部缓存、日志框架的格式化字符串、JSON 解析器的树节点。与其全局替换分配器,不如先定位真正肇事的模块。


















