new或malloc返回nullptr的主因是堆内存碎片化而非总量不足:多线程下arena管理与tcache延迟归还导致大量不可合并小空闲块,mallinfo()显示ordblks高、fordblks占比低即为典型碎片信号。

为什么 new 或 malloc 突然返回 nullptr,但内存总量明明够?
这不是“没内存”,而是堆空间被零散小块占据,无法满足当前请求的连续大小。多线程下尤其隐蔽:各线程频繁分配/释放不同尺寸对象,malloc 的 arena 管理策略(如 ptmalloc2)容易产生不可合并的间隙;再加上线程局部缓存(tcache、fastbins)未及时归还,全局碎片感知滞后。
关键判断依据:/proc/[pid]/maps 显示堆段([heap])很大,但 mallinfo 或 malloc_stats() 报告 ordblks(空闲块数)高、smblks(小块数)高、uordblks(已用字节数)却不高——典型碎片信号。
用 malloc_stats() 和 mallinfo() 快速确认是否为碎片问题
它们不线程安全,必须在单线程上下文或暂停所有分配线程后调用(例如通过信号 handler 捕获 SIGUSR1 并阻塞其他线程)。直接输出到 stderr 或写入临时文件,避免触发新分配。
-
mallinfo().ordblks> 1000 且mallinfo().fordblks占堆总大小比例异常低(如 -
mallinfo().smblks高 +mallinfo().hblks(分配的 heap 段数)持续增长,提示小块未合并、不断申请新段 -
malloc_stats()输出中出现大量 “fastbins”、“unsorted bin” 行,且 size 列分布离散(如 32、48、64、112 字节混杂),而非集中于某几个尺寸
用 LD_PRELOAD 替换 malloc 为 jemalloc 或 tcmalloc 验证是否缓解
glibc malloc 对多线程碎片敏感,而 jemalloc 的 per-CPU arena 和更激进的合并策略常能绕过问题。不是最终方案,但能快速验证是否为分配器行为导致。
立即学习“C++免费学习笔记(深入)”;
- 编译时链接:
g++ -o app app.cpp -ljemalloc,或运行时:LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 ./app - 启用 jemalloc 统计:
MALLOC_CONF="stats_print:true,abort_conf:true" ./app,关注输出中的 “allocated”, “active”, “mapped” 差值和 “bins” 分布 - 若切换后
new失败消失,且top中 RES 增长平缓,则基本锁定是 glibc malloc 的碎片管理缺陷
定位具体哪类对象在制造碎片:用 valgrind --tool=massif + 自定义分配器 hook
massif 默认不区分线程和分配点,需配合 --pages-as-heap=yes 和 --stacks=yes,再人工过滤堆栈中高频、小尺寸、生命周期短的分配模式。
- 启动命令:
valgrind --tool=massif --pages-as-heap=yes --stacks=yes --massif-out-file=massif.out ./app - 用
ms_print massif.out | grep -A5 "block" | head -20找出 top 几个 allocation tree,重点关注 size 在 16–256 字节、调用栈含std::vector::push_back、std::string构造、或自定义容器 resize 的节点 - 更准的方式:用
__attribute__((constructor))注册自定义mallochook(如malloc_hook),记录分配 size、调用栈(backtrace())、线程 ID 到环形缓冲区,失败时 dump —— 注意避免 hook 中再次 malloc
真正难的不是发现碎片,而是确认哪个线程在哪个循环里反复 new 64 字节又立刻 delete,还跨多个锁边界。这类模式往往藏在异步回调、定时器触发、或日志缓冲区 flush 路径中,得结合业务逻辑反推。


















