C++ new、堆分配与 brk / mmap

C++ new、堆分配与 brk / mmap

结论概要 :单次 operator new / malloc 往往在用户态分配器缓存 完成,不必然 触发内核态切换;仅当分配器需要向操作系统索取新的虚拟内存页 (如 brk/mmap 等路径)时,才会通过系统调用 陷入内核。具体分支依赖 libc 版本、分配器实现(ptmalloc / jemalloc / tcmalloc)与运行时参数

目录

  • [new 与用户态 / 内核态](#new 与用户态 / 内核态)
  • [语言层面的 new 形态](#语言层面的 new 形态)
  • [brk / sbrkmmap 对比](#brk / sbrk 与 mmap 对比)
  • 进程虚拟地址布局(概念)
  • [glibc malloc 决策(示意)](#glibc malloc 决策(示意))
  • [实测:观察 brkmmap 路径](#实测:观察 brk 与 mmap 路径)
  • [现代分配器的线程缓存(tcmalloc / jemalloc)](#现代分配器的线程缓存(tcmalloc / jemalloc))
  • 观测与调优线索
  • 参考链接

new 与用户态 / 内核态

概念 说明
用户态 应用程序常态执行;不能直接操作部分硬件与内核数据结构。
内核态 内核执行路径;通过系统调用、异常、中断从用户态进入。

典型 new T 流程 (非 placement、非定制 operator new 时,实现常基于 malloc 或等价堆分配):

  1. 分配存储 :向堆分配器申请一块足以存放 T 的内存。
  2. 构造 :在已分配存储上调用 T 的构造函数。

若分配器在已有 arena / 堆顶 / 线程缓存 内即可满足请求,则整个过程可在用户态完成;若需扩大堆顶新建匿名映射 ,则发生系统调用(如 brkmmap ),此时发生用户态 → 内核态切换。
内核态(经系统调用)
用户态
命中缓存
需向 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 / sbrkmmap 对比

维度 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_THRESHOLDARENA_MAX 等可用 mallopt 或环境/版本调整,不同 glibc 版本默认值可能不同
通常 yes


通常 no 大块
malloc(size) / 典型 operator new
size 小于 mmap 阈值?
tcache / fastbin / smallbin 等
能否在现有堆满足?
用户态返回指针
brk 等扩展堆
mmap 匿名映射

  • 小块路径 :尽量在线程缓存 / 堆 中完成,减少对 brk/mmap 的调用频率。
  • 大块路径 :直接 mmapfree 时可 munmap,减轻堆碎片与长期占用。

实测:观察 brkmmap 路径

下面程序用于示意 :小块分配后 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 ] 映射差异。

观察点 小块 大块(示意)
pbrk 关系 常在堆范围内,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 优先;不足时从 arenabin 批量填充
中央层 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 对比默认 mallocjemalloc/tcmalloc 在锁竞争、碎片上的差异
perf / strace -e brk,mmap,munmap 热点路径是否频繁 syscall (注意 strace 开销极大,仅短跑诊断)
应用层 减少分配次数(对象池、arena)、对齐大结构体、避免线程间频繁交叉 free

参考链接

内核页大小、mmap 阈值ASLRlibc 版本 均会影响实验输出;生产环境性能问题应结合 LD_PRELOAD 分配器perf、分配器统计与真实负载分析。

相关推荐
阿阿阿阿里郎2 小时前
C++面向对象--类、模板
c++
William_wL_2 小时前
【C++】list的使用
c++
鸠摩智首席音效师2 小时前
什么是 Unix / Linux 中的僵尸进程 ?
linux·服务器·unix
三万棵雪松2 小时前
【Linux 物联网网关主控系统-感知层部分(三)】
linux·物联网·嵌入式linux
Elnaij2 小时前
从C++开始的编程生活(25)——C++11标准Ⅱ
开发语言·c++
cjforever142 小时前
各STL容器的模拟实现
开发语言·数据结构·c++
云道轩2 小时前
在rocky linux 9.x上安装 Chrome
linux
小疙瘩2 小时前
VirtualBox 下 CentOS-10 下 Docker 安装 Mysql57 (包括 使用 Docker Compose 部署)
linux·docker·centos
郝学胜-神的一滴2 小时前
Linux高性能网络编程基石:epoll核心与文件描述符限制全解
linux·服务器·网络·c++·后端