C++ new、堆分配与 brk / mmap
结论概要 :单次 operator new / malloc 往往在用户态 由分配器缓存 完成,不必然 触发内核态切换;仅当分配器需要向操作系统索取新的虚拟内存页 (如 brk/mmap 等路径)时,才会通过系统调用 陷入内核。具体分支依赖 libc 版本、分配器实现(ptmalloc / jemalloc / tcmalloc)与运行时参数。
目录
- [
new与用户态 / 内核态](#new 与用户态 / 内核态) - [语言层面的 new 形态](#语言层面的 new 形态)
- [
brk/sbrk与mmap对比](#brk / sbrk 与 mmap 对比) - 进程虚拟地址布局(概念)
- [glibc
malloc决策(示意)](#glibc malloc 决策(示意)) - [实测:观察
brk与mmap路径](#实测:观察 brk 与 mmap 路径) - [现代分配器的线程缓存(tcmalloc / jemalloc)](#现代分配器的线程缓存(tcmalloc / jemalloc))
- 观测与调优线索
- 参考链接
new 与用户态 / 内核态
| 概念 | 说明 |
|---|---|
| 用户态 | 应用程序常态执行;不能直接操作部分硬件与内核数据结构。 |
| 内核态 | 内核执行路径;通过系统调用、异常、中断从用户态进入。 |
典型 new T 流程 (非 placement、非定制 operator new 时,实现常基于 malloc 或等价堆分配):
- 分配存储 :向堆分配器申请一块足以存放
T的内存。 - 构造 :在已分配存储上调用
T的构造函数。
若分配器在已有 arena / 堆顶 / 线程缓存 内即可满足请求,则整个过程可在用户态完成;若需扩大堆顶 或新建匿名映射 ,则发生系统调用(如 brk 、mmap ),此时发生用户态 → 内核态切换。
内核态(经系统调用)
用户态
命中缓存
需向 OS 要内存
new T / malloc
分配器快速路径
arena / tcache / bins
brk / mmap
建立或扩展映射
返回指针
| 概念 | 说明 |
|---|---|
| 虚拟地址已映射 ≠ 立刻占满物理内存 | 常为 按需缺页 (fault)时再配物理页;观测 RSS 与 commit 策略因 OS 而异 |
多次小 new |
往往只触达分配器数据结构;偶尔 才推进堆顶或 mmap |
语言层面的 new 形态
| 形式 | 与 brk/mmap 的关系(概括) |
|---|---|
new T(默认) |
通常调 operator new → 多实现转 malloc;后续讨论同本文 |
new (p) T(placement) |
不申请堆内存,仅在已有存储上构造 |
new T[n] / new (std::align_val_t) T |
可能走 对齐 或数组版 operator new ;仍多基于 malloc 家族 |
std::bad_alloc |
分配失败时抛出(除非使用 nothrow new) |
std::set_new_handler |
失败时可重试、记录日志或 abort;不改变「是否进内核」的本质 |
delete / free :小块常回到分配器空闲链表;未必 立刻 munmap 或收缩 brk,故进程 VSS 可能长期高于「当前存活对象」直觉。
brk / sbrk 与 mmap 对比
| 维度 | brk / sbrk(堆顶) |
mmap(匿名映射) |
|---|---|---|
| 作用位置 | 调整进程堆末端(program break) | 在虚拟地址空间中独立映射一段区域 |
| 与堆连续性 | 与既有堆连续延伸 | 一般为独立映射,不与堆顶连续 |
| 典型粒度 | 分配器内部再切分;向 OS 要页时常按页对齐 | 至少一页(如 4KiB);远小于一页的请求会浪费页内空间 |
| 释放归还 OS | 堆顶难以 随 free 立即收缩;常由分配器策略决定 |
munmap 可较直接解除映射 |
| 碎片与锁 | 长期小块分配易加剧堆碎片;多线程共享堆顶时需同步 | 大块独立映射有利于隔离 ;小块频繁 mmap/munmap 系统调用与 TLB 成本高 |
| 常见用途 | 传统 malloc 中小块扩展堆 |
大块 、独立生命周期 、posix_memalign 部分路径、内存映射文件等 |
sbrk 在可移植代码中已较少直接使用;Linux 上仍以 brk 为调整堆顶的系统调用语义核心。实际虚拟内存布局因内核、ASLR、mmap 映射顺序而异。
进程虚拟地址布局(概念)
text
低地址 ─────────────────────────────────────────────► 高地址
| 文本 / 只读数据 等 | ... | 堆 ──► brk(堆顶)| 间隙 | mmap 映射区(库、大块堆、匿名映射)... |
高地址侧常见区域
mmap 映射
libc / 栈 / 大块分配 ...
低地址侧
代码与常量等
已初始化数据 / BSS
堆
brk 向上增长
注意 :上图仅为教学示意;PIE/ASLR 会使各段基址每次运行变化,且 栈 增长方向与 mmap 落点依平台与内核策略而定。Linux 下可用 cat /proc/self/maps (或 pmap)对照本进程真实布局。
glibc malloc 决策(示意)
以下为 glibc ptmalloc 常见教材式简化;MMAP_THRESHOLD、ARENA_MAX 等可用 mallopt 或环境/版本调整,不同 glibc 版本默认值可能不同。
通常 yes
是
否
通常 no 大块
malloc(size) / 典型 operator new
size 小于 mmap 阈值?
tcache / fastbin / smallbin 等
能否在现有堆满足?
用户态返回指针
brk 等扩展堆
mmap 匿名映射
- 小块路径 :尽量在线程缓存 / 堆 中完成,减少对
brk/mmap的调用频率。 - 大块路径 :直接
mmap,free时可munmap,减轻堆碎片与长期占用。
实测:观察 brk 与 mmap 路径
下面程序用于示意 :小块分配后 sbrk(0) 读到的 program break 可能上移;1MiB 级别分配常落在远离堆 的地址且 brk 不变 (表明走了 mmap )。请以本机 glibc 与 pmap 输出为准。
c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
printf("Initial brk = %p\n", sbrk(0));
void *p1 = malloc(64);
printf("After malloc(64): p1=%p, brk=%p\n", p1, sbrk(0));
void *p2 = malloc(1024u * 1024u);
printf("After malloc(1MiB): p2=%p, brk=%p\n", p2, sbrk(0));
free(p1);
free(p2);
return 0;
}
编译运行示例:gcc -O0 demo.c -o demo && ./demo
可用 pmap -x <pid> (在另一终端对运行中进程)查看 [heap] 与 [ anon ] 映射差异。
| 观察点 | 小块 | 大块(示意) |
|---|---|---|
p 与 brk 关系 |
常在堆范围内,brk 可能增大 |
地址常落在 mmap 映射区 ,brk 可不变 |
现代分配器的线程缓存(tcmalloc / jemalloc)
共同目标 :让高频小对象 的 malloc/free 尽量在本线程上下文 完成,减少全局锁与系统调用;仅在缓存空或满 时与中央结构 或 OS 交互。
多级结构(概念)
操作系统
跨线程共享
每线程
批量搬运
申请与归还页
线程缓存
(tcache / thread cache)
中央缓存 / Arena bins
页堆 / Chunk
brk、mmap 等
tcmalloc 与 jemalloc(纲要)
| 项目 | tcmalloc(概念) | jemalloc(概念) |
|---|---|---|
| 小对象 | 按 size class 进入线程本地 free list ;不足时向 central freelist 批量取 | tcache 优先;不足时从 arena 的 bin 批量填充 |
| 中央层 | Central free list,协调多线程与页堆 | 多个 arena,线程绑定/轮询降低锁竞争 |
| 大块 | 常绕开线程缓存,在页级结构用更粗粒度同步 | huge 类对象可走 mmap/munmap,减少与普通堆混杂 |
| 演进 | 新版本有 per-CPU cache 等优化方向(以官方文档为准) | extent / slab 等设计控制碎片与元数据开销 |
注意 :「无锁」多为快速路径上无全局互斥 ;跨线程归还、中央层批量移动仍可能有原子或锁。具体数据结构与阈值以 gperftools / jemalloc 对应版本说明为准。
观测与调优线索
| 手段 | 能回答的问题 |
|---|---|
/proc/<pid>/maps / pmap -x |
堆、[anon]、[heap] 等映射范围与是否出现大块 mmap |
mallinfo / malloc_stats(glibc) |
分配器内部视角(部分接口已标记过时,仅作粗查) |
LD_PRELOAD=libjemalloc.so 等 |
对比默认 malloc 与 jemalloc/tcmalloc 在锁竞争、碎片上的差异 |
perf / strace -e brk,mmap,munmap |
热点路径是否频繁 syscall (注意 strace 开销极大,仅短跑诊断) |
| 应用层 | 减少分配次数(对象池、arena)、对齐大结构体、避免线程间频繁交叉 free |
参考链接
- 相关主题公开文章(背景阅读):https://mp.weixin.qq.com/s/dEIG1RgGVZWscfe6K4MGUA
mallopt:man mallopt(Linux)- jemalloc 设计文档:https://jemalloc.net/
- tcmalloc(gperftools):https://github.com/gperftools/gperftools
内核页大小、mmap 阈值 、ASLR 与 libc 版本 均会影响实验输出;生产环境性能问题应结合 LD_PRELOAD 分配器 、perf、分配器统计与真实负载分析。