当前位置: 首页 > ds >正文

内存分配器ptmalloc2、tcmalloc、jemalloc,结构设计、内存分配过程详解

1. 引言

博主之前做过一个高并发内存池的项目实践,在实践中对于内存分配器的内存分配过程理解更加深刻了。在此期间,翻查了不少资料以及博客,发现源码分享的博客不多,能生动完整的讲述ptmalloc2、tcmalloc、jemalloc它们的结构设计以及内存分配过程的博客更是少之又少。那么在这篇文章中博主将总结分享内存分配器ptmalloc2、tcmalloc、jemalloc,欢迎大家留言讨论!

2. 现状

目前大部分服务端程序使用glibc提供的 malloc/free 系列函数,而glibc使用的 ptmalloc2 在性能上远远弱后于googletcmallocfacebookjemalloc。 而且后两者只需要使用LD_PRELOAD环境变量启动程序即可,甚至并不需要重新编译。

3. 业务场景

分配内存时进行系统调用的接口,对 heap 的操作, 操作系统提供了 brk() 系统调用,设置了Heap的上边界; 对 mmap 映射区域的操作,操作系统供了 mmap()munmap() 函数。

因为系统调用的代价很高,不可能每次申请内存都从内核分配空间,尤其是对于小内存分配。 而且因为 mmap 的区域容易被 munmap 释放,所以一般大内存采用 mmap(),小内存使用 brk()。

4. glibc ptmalloc2

最新版本:作为Glibc的默认内存分配器,ptmalloc2的更新与Glibc(目前更新到2.35+)版本绑定。截至当前,其核心设计仍基于最初的ptmalloc2架构, glibc 2.26(2017年)开始引入了线程本地缓存(tcache,Thread Local Caching)。

4.1 内存管理结构

线程本地缓存(tcache)
  • 作用:无锁快速分配小内存(每个线程独立)。

  • 管理范围:默认 ≤ 1032 字节(64 位系统)。

  • 数据结构

    • 64 个 tcache_bin(单链表,LIFO),每个 bin 对应一种固定大小(8B~1032B)。

    • 每个 bin 最多缓存 7 个 chunk(可通过 GLIBC_TUNABLES 调整)。

  • 优化点

    • 优先从 tcache 分配/释放,完全无锁。

    • 若 tcache 为空,则从 fastbins/smallbins 批量预加载(加锁)。

Fast Bins(快速通道)
  • 作用:缓存最近释放的小内存(仍保留 chunk 未合并,加速复用)。

  • 管理范围:≤ 160 字节(64 位系统)。

  • 数据结构:7 个单链表(LIFO),每个链表存储相同大小的 chunk。

  • 与 tcache 的关系

    • tcache 不足时从 fastbins 补充。

    • free() 时,小 chunk 可能先进入 tcache,若 tcache 满则进入 fastbins

Small Bins(小内存通道)
  • 作用:管理中等大小的内存(固定大小,合并友好)。

  • 管理范围:≤ 1008 字节(64 位系统)。

  • 数据结构:62 个双向链表(FIFO),每个 bin 对应一种固定大小(如 16B, 24B, ..., 1008B)。

  • 与 tcache 的关系

    • tcache 不足时从 smallbins 分配。

    • free() 时,若 chunk 大小匹配且 tcache 满,则进入 smallbins

Large Bins(大内存通道)
  • 作用:管理大内存块(范围区间,按大小排序)。

  • 管理范围:> 1008 字节。

  • 数据结构:63 个双向链表,每个 bin 管理一个大小范围(如 1024B~1088B, 1089B~1152B...)。

  • 分配策略

    • 查找最小满足大小的 chunk(可能分割)。

    • 若找不到,则进入 unsorted bin 或 top chunk

Unsorted Bin(临时缓冲区)
  • 作用:临时存放释放的 chunk,等待重新分类。

  • 数据结构:1 个双向链表(FIFO),存放最近释放的 chunk(不分大小)。

  • 分配流程

    • 分配时优先检查遍历 unsorted bin,若找到合适 chunk 直接返回,否则将其转移到 small/large bins

Top Chunk(堆顶块)
  • 作用:当前堆的顶部未分配内存,用于动态扩展。

  • 分配逻辑

    • 当所有 bins 无法满足请求时,从 Top Chunk 切割所需内存。

    • 若 Top Chunk 不足,通过 sbrk(主分配区)或 mmap(非主分配区)扩展堆空间。

mmap 通道(超大内存)
  • 管理大小:默认 ≥ 128KB(可通过 M_MMAP_THRESHOLD 调整)。

  • 特点

    • 直接通过 mmap 分配独立内存段,绕过堆管理。

    • 释放时通过 munmap 立即归还系统,避免碎片。

last remainder chunk(最后剩余块)
  • 优化连续内存分配:当用户请求的内存略小于某个空闲块时,分割后剩余的碎片会被标记为 last remainder,优先用于后续的连续分配请求。

  • 减少外部碎片,提升内存利用率。

4.2 内存分配流程

4.3 多线程支持

  • 主分配区(Main Arena):主线程默认使用,通过 brk 扩展堆。

  • 非主分配区(Thread Arena):其他线程按需创建(数量受 MALLOC_ARENA_MAX 限制)。

  • 锁优化

    • 每个分配区有独立的锁,线程优先访问绑定的分配区。

    • 若所有分配区被占用,线程会竞争主分配区。

4.4 缺点

  • 内存碎片:外部和内部碎片问题突出,尤其长期运行后。
  • 锁竞争:尽管有 tcache 和分配区,高并发下仍可能成为瓶颈。
  • 释放延迟:fastbins 延迟合并,brk 堆收缩不积极。
  • 场景局限:不适合高频小对象分配或实时系统。

5. tcmalloc

最新版本:截至2025年,tcmalloc已迭代至支持per-CPU模式的优化版本(如v2.10+),进一步提升多核性能。场景适用于多核服务器(如 Web 服务、数据库),高频小对象分配(如微秒级响应的 RPC 框架)。

5.1 内存管理结构

CPU Local Cache(CPU 本地缓存)

  • 申请对象为小内存(通常 ≤256KB)。

  • 使用Size Class 哈希桶,每个 CPU 核心维护一组自由链表(FreeList),按大小分类(如 8B、16B、...、256KB)。

  • 动态批量填充,当本地缓存不足时,从下层批量获取对象(类似慢启动算法)。

  • 这里无锁

Central Free List(中心空闲列表)
  • 这一层全局共享,作为 CPU 本地缓存的后备仓库。

  • 结构为哈希桶Span 链表:按大小分类管理内存块(Span),每个 Span 被切分为多个小对象。

  • 轻量级锁:仅当 CPU 缓存需要补充时加锁(锁粒度细化到 Size Class)。

  • 申请内存,当CPU 缓存耗尽时,从 Central Free List 批量拉取对象(如一次获取 32 个 16B 的块)。Central Free List 自身从 PageHeap 申请新的 Span。

PageHeap(页堆)

  • 以页(通常 8KB)为粒度的大内存。

  • 合并相邻空闲页,减少外碎片。

  • 基数树(Radix Tree)算法,快速映射内存页到 Span,支持 O(1) 查找。
  • 超过 256KB 的请求直接由 PageHeap 处理(通过 mmap 或 VirtualAlloc)。

5.2 内存分配流程(Per-CPU 模式)

  1. 小内存分配(≤256KB)时,线程先获取当前 CPU 的本地缓存(GetThisCPUCache()),根据请求大小找到对应的 Size Class 自由链表。若链表非空,直接弹出对象返回(无锁),若链表为空,从 Central Free List 批量获取对象(加锁,但频率低)。
  2. 小内存释放时,将对象放回当前 CPU 的本地缓存自由链表(无锁)。若本地缓存超过阈值(如 1024 个对象),触发批量归还到 Central Free List。

  3. 大内存分配(>256KB)时,直接由 PageHeap 分配,通过 mmap 或 VirtualAlloc 申请大块内存。
  4. 大内存释放时通过 PageHeap 的基数树找到对应 Span,标记为空闲。尝试合并相邻空闲 Span,形成更大的连续内存。

5.4 局限性

它是零锁竞争的,Per-CPU 缓存完全无锁,适合超高并发场景。本地化操作减少 CPU 缓存行失效(Cache Line Ping-Pong),所以它延迟低。支持弹性扩展,CPU 数量增加时,性能线性提升(无全局瓶颈)。

Per-CPU 缓存可能暂存未使用的对象(可通过调优阈值缓解),造成内存浪费。它对CPU比较依赖,若线程频繁迁移 CPU,性能会下降(需绑定线程到核心)。

6. jemalloc

最新版本jemalloc 5.3.0(发布于2025年5月1日),进一步优化内存碎片管理和多线程扩展性。

6.1 内存管理结构

之前的版本

在之前的版本中内存被划分成若干个arena;每个arena被划分成若干个chunk(每个默认为4M);每个chunk被划分成若干个run;每个run由若干个page组成(每个page默认为4K),同一个run中的page又被细分成若干个大小相同的region;

为了减少内存碎片及快速定位到合适大小的内存,jemalloc将run按以下class_size分成44类(同一个run下的region大小相同):

  • small(<4K): [8], [16, 32, 48, …, 128], [192, 256, 320, …, 512], [768, 1024, 1280, …, 3840]

  • large(4K-4M): [4K, 8K, 12K, …, 4072K]

  • huge(>4M): [4M, 8M, 12M, …]

  • 对于小于4K内存的申请,jemalloc直接向上取整到最小的class_size,例如申请1-7字节,都会分配一个8字节内存。

新版本

在 jemalloc 5.3.0 中,旧版的 chunkrun 等概念被 Extent 统一取代,其核心设计如下:

1. Extent(扩展块)
  • 表示连续的虚拟内存区域(大小灵活,通常为 2MB 或 4MB,可动态调整)。替代旧版的 chunk,支持更细粒度的拆分与合并。

  • 状态分级:分为 Active(在用)、Dirty(已释放但未擦除)、Muzzy(部分擦除)、Retained(完全擦除保留)。

  • 动态管理:支持按需分割为 Slab 或直接分配。

2. Slab(内存板)
  • 将 Extent 划分为固定大小的槽位(如 32B、64B),通过位图管理分配状态。替代旧版 run 的功能,但更轻量化。

  • 按需分配:仅在使用时初始化 Slab,减少内存占用。

  • 延迟释放:空闲 Slab 不会立即合并,保留在 Dirty/Muzzy 状态供快速复用。

3. Page(页)
  • 保留它的意义,仍作为操作系统内存管理的最小单位(如 4KB),但 jemalloc 内部通过 Extent 聚合多页。

  • 用户无需关注页级操作,所有分配均通过 Extent 和 Slab 抽象。

4. Region(区域)
  • 旧版中模糊的 region 概念被 Extent 和 Slab 明确取代,不再存在于官方文档。

6.2 内存分配流程

1. 从线程缓存(Thread Cache)分配
  • 当请求的内存大小 ≤ 14KB(小对象)。线程本地缓存(TLS)中对应的 Size Class 有空闲槽位。根据请求大小匹配对应的 Size Class(如 32B、64B),从线程本地的 Slab 空闲链表 中弹出第一个空闲槽位,直接返回槽位内存地址(无锁操作)。

  • 这个过程是零锁竞争,速度极快(纳秒级)。适合高频小对象分配(如短生命周期对象)。

2. 从 Arena 分配
  • 当线程缓存为空,且请求大小 ≤ 4MB(中等对象)。或请求大小 >14KB 但 ≤4MB(跳过线程缓存)。

  • 线程绑定到特定 Arena(默认轮询或哈希分配)。若当前 Arena 锁竞争激烈,可能创建新 Arena。从 Arena 的 Extent/Slab 分配,如果是中等对象(14KB < size ≤4MB),直接分配整个 Extent 或从空闲 Extent 链表分割。如果是小对象(≤14KB):从 Arena 的 Slab 中批量获取多个槽位,填充线程缓存后返回一个。

  • 锁机制:Arena 内部使用细粒度锁(如每个 Extent 独立锁),减少竞争。

3. 从系统内存(mmap/brk)分配
  • 当我们请求大小 >4MB(大对象)或 Arena 的 Extent 不足(需扩展堆空间)时。直接调用 mmap,记录元信息到全局基数树(Radix Tree)。通过 mmap 申请独立内存段(默认 ≥4MB)。这时是大对象,不进入线程缓存或 Arena,直接由全局结构跟踪。释放时调用 munmap 立即归还系统,避免碎片。

  • 可以避免污染线程缓存和 Arena。减少大内存的合并开销。

6.3 内存回收机制

1. 线程本地缓存(Thread Cache)的回收

  • 用户释放的小对象(≤4KB)首先存入线程本地缓存(Thread Cache)的对应 size class 槽位。

  • 批量回收每个 size class 的 Thread Cache 槽位有最大保留数量(如默认 200 个)。当超出阈值时,jemalloc 将多余的槽位批量归还到全局的 Arena

2. 全局分配区(Arena)的回收

  • 每个 Arena 维护一组 extent(如 4MB 的大块内存),按 size class 分类。Extent 分割为 slab 后,jemalloc 跟踪每个 slab 中已分配和空闲的槽位(Slot)。

  • Slab 完全空闲时,即当某个 slab 的所有槽位均被释放时,其所属的 Extent 可能被标记为空闲。jemalloc 定期内存压力检测或根据系统内存压力,合并空闲 Extent 并归还操作系统。

3. Extent 的合并与归还

  • 惰性合并(Lazy Coalescing)jemalloc 不会立即合并相邻的空闲 Extent,而是保留它们以便快速响应未来可能的相同大小请求。当连续的空闲 Extent 达到一定数量或系统内存不足时,主动合并以减少外部碎片。

  • 操作系统归还完全空闲的 Extent 可能通过 munmap 归还操作系统。jemalloc 默认保留部分空闲 Extent(通过 retain:true 配置),避免频繁的 mmap/munmap 系统调用开销。

6.4 局限性

  • 内存碎片积累(尤其多 Arena 场景)。
  • 线程本地缓存囤积 导致内存利用率下降。
  • 超大对象管理开销 和 配置复杂度高
  • 平台兼容性差异 和 调试困难

参考资料:

内存优化总结:ptmalloc、tcmalloc和jemalloc_ptmalloc jemalloc tcmalloc-CSDN博客

ptmalloc VS tcmalloc VS jemalloc 原理及对比测试

ptmalloc代码浅析2(small bin/large bin结构图)_largebin-CSDN博客


本篇文章对内存分配器ptmalloc2、tcmalloc、jemalloc,结构设计、内存分配过程做了详解!欢迎留言讨论!

http://www.xdnf.cn/news/6399.html

相关文章:

  • Cesium.Ray 知识详解,示例代码
  • 实验六:按键模拟控制实现
  • Java—— 可变参数、集合工具类、集合嵌套、不可变集合
  • 十个免费试用的云数据库
  • Awesome WM自定义菜单实现nas共享目录挂载
  • K8S Ingress 实现AB测试、蓝绿发布、金丝雀(灰度)发布
  • 基于大模型预测的全面惊厥性癫痫持续状态技术方案大纲
  • 力扣-98.验证二叉搜索树
  • C# winform 日志 NLog
  • 【vue】脚手架
  • 瀑布模型VS敏捷模型VS喷泉模型
  • 【Linux】多路转接epoll、Linux高并发I/O多路复用
  • SpringAI
  • 印度尼西亚数据源对接技术指南
  • YOLOv11融合[CVPR2025]OverLock中的模块
  • 合并有重叠的时间区间的极简方法
  • 【证书与信任机制​】​​SSL证书类型全解析:DV、OV、EV的区别与应用场景
  • 【C#基础】集合.Any() 与 判断集合的长度有啥区别?
  • atoi函数,sprintf函数,memcmp函数,strchar函数的具体原型,功能,返回值;以及使用方法
  • 现代计算机图形学Games101入门笔记(六)
  • 19、云端工业物联网生态组件 - 工厂能效与预测维护 - /数据与物联网组件/cloud-iiot-factory-analysis
  • 紫外波段太阳光模拟器介绍
  • Python Matplotlib 库【绘图基础库】全面解析
  • 在UI 原型设计中,交互规则有哪些核心要素?
  • 数据统计分析及可视化
  • 开源 Web Shell 工具
  • 万文c++继承
  • 前端表格滑动滚动条太费事,做个浮动滑动插件
  • Java基于SpringBoot的外卖系统小程序【附源码、文档说明】
  • 功能连接计算的科学选择:静息态fMRI中20种指标的全面评估