编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程虚拟地址空间
      • 栈与堆底层对决
      • 指针本质与多级解引
      • 指针运算底层真相
      • 函数指针与回调机制
      • 限定符与指针语义
      • 补码与位运算原理
      • IEEE754浮点本质
      • 数组与指针的纠葛
      • 结构体对齐与优化
      • 字符串存储与安全
      • 预处理器宏与条件编译
      • 编译到汇编全流程
      • 链接器符号与重定位
      • 静态库与动态库对比
      • Make与CMake构建
      • 文件IO与系统调用
      • 动态内存管理揭秘
        • 目录
        • 1. 案例引入
          • 1.1 内存只涨不降
          • 1.2 顺藤摸到根因
          • 1.3 我们要回答什么
        • 2. 架构概览
          • 2.1 堆三层模型
          • 2.2 ptmalloc2 的核心设计哲学
        • 3. ptmalloc2 chunk 结构
          • 3.1 malloc_chunk 的字段布局
          • 3.2 prev_size 的复用技巧
          • 3.3 size 字段的 3 个标志位
          • 3.4 从指针到 chunk 的地址换算
        • 4. 三级 Bins 分配策略
          • 4.1 FastBins
          • 4.2 SmallBins
          • 4.3 LargeBins
          • 4.4 UnsortedBin
          • 4.5 tcache
        • 5. 双通道扩展机制
          • 5.1 sbrk 小块内存的 arena 扩展
          • 5.2 mmap 大块内存的直接映射
          • 5.3 MMAP_THRESHOLD 的动态调整
          • 5.4 多线程 arena 竞争与缓解
        • 6. 碎片与合并问题
          • 6.1 内部碎片 vs 外部碎片
          • 6.2 free 时的前后合并逻辑
          • 6.3 为什么 RSS 只涨不降
          • 6.4 malloctrim 与 mallocstats
        • 7. 内存池设计与实现
          • 7.1 定长分配器
          • 7.2 完整实现与性能测试
          • 7.3 Valgrind 内存泄漏检测原理
          • 7.4 ASan RedZone 影子内存原理
        • 8. 综合案例串讲
          • 8.1 案例真相揭晓
          • 8.2 一次 malloc 的完整旅程
          • 8.3 面试高频问题清单
          • 8.4 内存管理速查卡
      • 未定义行为与防御
      • C工程化与设计哲学
    • 标准集库

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • C语言入门精通
  • 专栏博客
杨充
2026-06-10
目录

动态内存管理揭秘

# 18.动态内存管理揭秘

glibc ptmalloc2 的 chunk 数据结构、Fast Bins/Small Bins/Large Bins 三级分配策略、sbrk 小内存 vs mmap 大内存双通道、空闲 chunk 合并与碎片问题、Valgrind --leak-check=full 内存泄漏检测、AddressSanitizer 影子内存 RedZone 原理、手写内存池:固定大小分配器的实现与性能对比

# 目录

  • 1. 案例引入
    • 1.1 内存只涨不降
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构概览
    • 2.1 堆三层模型
    • 2.2 ptmalloc2 的核心设计哲学
  • 3. ptmalloc2 chunk 结构
    • 3.1 malloc_chunk 的字段布局
    • 3.2 prev_size 的复用技巧
    • 3.3 size 字段的 3 个标志位
    • 3.4 从指针到 chunk 的地址换算
  • 4. 三级 Bins 分配策略
    • 4.1 FastBins
    • 4.2 SmallBins
    • 4.3 LargeBins
    • 4.4 UnsortedBin
    • 4.5 tcache
  • 5. 双通道扩展机制
    • 5.1 sbrk 小块内存的 arena 扩展
    • 5.2 mmap 大块内存的直接映射
    • 5.3 MMAP_THRESHOLD 的动态调整
    • 5.4 多线程 arena 竞争与缓解
  • 6. 碎片与合并问题
    • 6.1 内部碎片 vs 外部碎片
    • 6.2 free 时的前后合并逻辑
    • 6.3 为什么 RSS 只涨不降
    • 6.4 malloc_trim 与 malloc_stats
  • 7. 内存池设计与实现
    • 7.1 定长分配器
    • 7.2 完整实现与性能测试
    • 7.3 Valgrind 内存泄漏检测原理
    • 7.4 ASan RedZone 影子内存原理
  • 8. 综合案例串讲
    • 8.1 案例真相揭晓
    • 8.2 一次 malloc 的完整旅程
    • 8.3 面试高频问题清单
    • 8.4 内存管理速查卡

# 1. 案例引入

# 1.1 内存只涨不降

某广告投放引擎是一个 7×24 的常驻进程,上线 3 个月后运维发现它的内存占用曲线是这样的:

RSS
^
│                                     ╱
│                               ╱╲╱╱
│                          ╱╲╱╱
│                     ╱╲╱╱
│                ╱╲╱╱
│           ╱╲╱╱
│      ╱╲╱╱
│ ╱╲╱╱
└──────────────────────────────────────────► 时间
  0         1天       2天       3天       4天...

# 阶梯状上涨!但代码里所有 malloc 都有对应的 free
1
2
3
4
5
6
7
8
9
10
11
12
13
14

用 pmap 看进程的内存映射:

$ pmap -x $PID | head -20
Address           Kbytes     RSS   Dirty Mode  Mapping
...
0000000001234000  20480    20480   20480 rw---   [ anon ]   ← 堆持续增大
00007f3a8b400000   2048      512       0 r-x-- libc.so.6
...
1
2
3
4
5
6

现象:

  • 每处理一批广告请求,RSS 上涨 ~50MB
  • 但这批请求处理完后,RSS 不降
  • 下次请求高峰,RSS 再涨 ~50MB
  • 如此反复,呈阶梯状永久上涨
  • 进程从来不崩(没有 segfault),但 K8s 的 OOM Killer 会在 RSS 接近 limit 时杀掉它

用 malloc_stats() 查看 glibc 内部分配器状态:

#include <malloc.h>
malloc_stats();
// 输出:
// Arena 0:
// system bytes     =  524288000  (500 MB 从系统申请的)
// in use bytes     =    8388608  (8 MB 实际在用)
// Total (incl. mmap):
// system bytes     =  524288000
// in use bytes     =    8388608
// max mmap regions =          8
// max mmap bytes   =  134217728
1
2
3
4
5
6
7
8
9
10
11

system bytes = 500MB,in use bytes = 8MB——492MB 是空闲的,但 glibc 没有归还给操作系统!

# 1.2 顺藤摸到根因

追查:

  • 假设 1:是不是内存泄漏?—— valgrind --leak-check=full 跑一轮测试,报告 "All heap blocks were freed -- no leaks are possible"。没有泄漏。
  • 假设 2:那为什么 RSS 不降?—— free 只把内存还给 glibc 的分配器(进入 bin),不一定还给操作系统(munmap 或 sbrk 缩堆)。glibc"囤"着空闲内存以备后续 malloc 复用。
  • 假设 3:为什么囤着?—— sbrk 扩展堆是单向的:只能往上推,不能局部释放——除非堆顶恰好全是空闲 chunk,否则无法缩堆。
  • 假设 4:广告请求的模式:先 malloc 2000 个小对象(每个 50-200B),处理完 free 掉。这些对象散布在堆中,堆中间有一两个大对象还没释放 → 堆顶退不下来。
  • 假设 5:下一次请求又 malloc 2000 个新对象 → glibc 从 bin 里拿空闲 chunk 复用。但如果 bin 里的 chunk 不够大(碎片化),glibc 会从堆顶分配新的 → 堆又涨了。

凶手:内存碎片(fragmentation)——大量小对象的 free 和 re-malloc 在堆中留下"洞",glibc 无法把分散的空闲 chunk 合并成连续的大块归还给 OS。堆的 RSS 只涨不降。

这个事故藏着至少 8 个原理点:

① malloc 返回的指针背后是什么数据结构?chunk 长什么样?    → 第 3 章
② free 后内存去哪了?Fast Bins/Small Bins/Large Bins?    → 第 4 章
③ 什么时候用 sbrk 扩展堆,什么时候用 mmap?               → 第 5 章
④ 为什么 free 了但 RSS 不降?碎片与合并机制?             → 第 6 章
⑤ tcache 是什么?glibc 2.26 的性能提升从哪来?            → 第 4.5 节
⑥ Valgrind 怎么检测内存泄漏?                             → 第 7.3 节
⑦ AddressSanitizer 的 RedZone 怎么工作?                 → 第 7.4 节
⑧ 自己写一个内存池比 malloc 快多少?                      → 第 7.1/7.2 节
1
2
3
4
5
6
7
8

# 1.3 我们要回答什么

从malloc(100)到这一百字节被分配出去——这一秒之内,glibc 的 ptmalloc2 分配器做了大量决策。它比你想的复杂:chunk 的设计精妙到用 3 个 bit 省了 3 个字段,bin 的组织方式精确到用 64 个单链表 + 89 个循环链表 + tcache 做分层缓存。

本篇路线:

架构总图 (第 2 章)
   ↓
chunk 结构 (第 3 章) ─→ 解开"malloc 返回的指针到底是什么"
   ↓
三级 Bins (第 4 章) ─→ 解开"free 后的内存去了哪个桶"
   ↓
双通道扩展 (第 5 章) ─→ 解开"sbrk 和 mmap 的分工"
   ↓
碎片与合并 (第 6 章) ─→ 解开"为什么 RSS 只涨不降"
   ↓
内存池实战 (第 7 章) ─→ 自己写一个比 malloc 快 10 倍的分配器
   ↓
综合案例 (第 8 章) ─→ 彻底剖开 + 速查卡
1
2
3
4
5
6
7
8
9
10
11
12
13

📌 本篇定位:这是"内存"话题的收尾篇。第 01 篇讲了"内存在哪(地址空间)",第 17 篇讲了"文件 IO 怎么经过内核",本篇讲"堆内存怎么被分配和回收"。理解 ptmalloc2 的内部机制,是写出不会泄漏、不会碎片化、不会 OOM 的 C 程序的前提。

# 2. 架构概览

# 2.1 堆三层模型

┌─────────────────────────────────────────────────────────────────┐
│                      应用层 (Application)                        │
│                                                                 │
│  用户代码: malloc(100) / free(ptr) / realloc / calloc          │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│                      分配器层 (Allocator)                        │
│                                                                 │
│  glibc ptmalloc2:                                               │
│    ┌───────────────────────────────────────────────────────┐    │
│    │  tcache (线程本地缓存, glibc 2.26+)                    │    │
│    │  Fast Bins (≤80B, LIFO 单链表, 不合并)               │    │
│    │  Small Bins (≤512B, 双向循环链表)                      │    │
│    │  Large Bins (>512B, 排序链表, 最佳适配)                 │    │
│    │  Unsorted Bin (新释放的 chunk 中转站)                  │    │
│    │  Top Chunk (堆顶未分配区域)                              │    │
│    └───────────────────────────────────────────────────────┘    │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│                      系统层 (OS)                                  │
│                                                                 │
│  brk/sbrk: 小块 → 扩展堆顶 (program break)                     │
│  mmap/munmap: 大块 → 匿名映射到 mmap 区域                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

关键边界:

  • malloc(100) 不一定触发系统调用——可能从 tcache/bin 直接拿
  • free(ptr) 不一定会 munmap——通常只是把 chunk 放回 bin 链表
  • 分配器是用户态"缓存层"——它在应用和 OS 之间缓冲,减少系统调用

# 2.2 ptmalloc2 的核心设计哲学

ptmalloc2 (glibc 的 malloc 实现,基于 Doug Lea 的 dlmalloc) 的设计目标:

目标 实现手段
低延迟 tcache 无锁、fastbins LIFO、大小分级
低碎片 chunk 合并(前后相邻空闲 chunk 合并)、双通道(小/大分离)
多线程友好 每线程一个 arena、tcache 线程本地
空间节省 prev_size 复用、chunk header 只有 16 字节
安全可靠 MALLOC_CHECK_ 环境变量、MALLOC_PERTURB_ 填充 freed 内存

# 3. ptmalloc2 chunk 结构

# 3.1 malloc_chunk 的字段布局

疑惑:malloc(100) 返回了一个 void* 指针,这块内存"前面"有什么?

论证——每个 malloc 返回的指针,前面都有一个 chunk header:

返回给用户的指针 ptr = malloc(100)
                          │
                          ▼
┌─────────┬─────────┬─────────┬─────────────────────────────────┐
│ prev_size│  size   │  fd     │  用户数据 (100 字节)             │
│  (8B)    │  (8B)    │  (8B)   │                                │
├─────────┴─────────┼─────────┴─────────────────────────────────┤
│  ← chunk header → │  ← 用户可用区域 →                        │
│     (16 字节)      │     (至少 100 字节, 对齐到 16)            │
└───────────────────┴───────────────────────────────────────────┘
                      ▲
                      │
              实际分配的 chunk (从上一个 chunk 的边界开始)
1
2
3
4
5
6
7
8
9
10
11
12
13

malloc_chunk 结构体定义(glibc 源码 malloc/malloc.c,简化):

struct malloc_chunk {
    INTERNAL_SIZE_T mchunk_prev_size;  // 前一个 chunk 的大小
                                        // (如果前一个是空闲的)

    INTERNAL_SIZE_T mchunk_size;       // 当前 chunk 的大小
                                        // (低 3 bits 是标志位)

    // 当 chunk 空闲时,以下字段用于链表管理:
    struct malloc_chunk* fd;           // 前向指针 (bin 双向链表)
    struct malloc_chunk* bk;           // 后向指针 (bin 双向链表)

    // 当 chunk 是 Large Bin 空闲块时:
    struct malloc_chunk* fd_nextsize;  // 下一个不同大小的 chunk
    struct malloc_chunk* bk_nextsize;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

当 chunk 被分配(in use)时,fd/bk 的位置被用户数据覆盖——这是 glibc 高空间利用率的设计。

# 3.2 prev_size 的复用技巧

prev_size 字段有一个巧妙的复用规则:

如果前一个 chunk (P) 是空闲的:
  → 当前 chunk 的 prev_size = 前一个 chunk 的 size
  → 用于在 free 时快速计算前一个 chunk 的起始地址

如果前一个 chunk (P) 正在使用中:
  → prev_size 字段被前一个 chunk 的用户数据"借用"
  → glibc 不浪费这 8 字节!
1
2
3
4
5
6
7

这个"借用"是 glibc 把 chunk 开销控制在 16 字节的关键——prev_size 只有在前一个 chunk 空闲时才需要存储,in-use 时可以借给用户数据。

# 3.3 size 字段的 3 个标志位

mchunk_size 的低 3 位被用作标志(因为 chunk 大小总是 16 字节对齐,低 4 位永远为 0):

mchunk_size (64位):
┌──────────────────────────────────────────────────────┬───┬───┬───┐
│                                                      │ A │ M │ P │
│               chunk 大小 (61 bits)                        │   │   │
└──────────────────────────────────────────────────────┴───┴───┴───┘
                                                                  ↑
                                                            bit 0: PREV_INUSE
                                                            bit 1: IS_MMAPPED
                                                            bit 2: NON_MAIN_ARENA
1
2
3
4
5
6
7
8
9
标志 位 含义 用途
PREV_INUSE (P) bit 0 前一个 chunk 是否在使用中 free 时决定是否向前合并
IS_MMAPPED (M) bit 1 这个 chunk 是否由 mmap 分配 free 时决定 munmap 还是放回 bin
NON_MAIN_ARENA (A) bit 2 是否属于非主 arena free 时确定归还到哪个 arena
// 宏定义
#define PREV_INUSE     0x1
#define IS_MMAPPED     0x2
#define NON_MAIN_ARENA 0x4

// 从 size 中提取标志和大小
#define chunk_size(p)  ((p)->mchunk_size & ~(SIZE_BITS))
#define prev_inuse(p)  ((p)->mchunk_size & PREV_INUSE)
1
2
3
4
5
6
7
8

# 3.4 从指针到 chunk 的地址换算

// 用户拿到的指针: ptr = malloc(100)
// 到 chunk header 的转换:
#define mem2chunk(mem)  ((mchunkptr)((char*)(mem) - 2*SIZE_SZ))

// 从 chunk header 到用户数据区域:
#define chunk2mem(p)    ((void*)((char*)(p) + 2*SIZE_SZ))
1
2
3
4
5
6

地址换算图解:

低地址 ──────────────────────────────────────► 高地址

┌──────────┬──────────┬──────────────────────┐
│ prev_size │   size   │   用户数据 (100B)     │
│   (8B)    │   (8B)   │                      │
└──────────┴──────────┴──────────────────────┘
     ▲                     ▲
     │                     │
   chunk ptr            返回的 user ptr
     │                     │
     └────── 16B ──────────┘
1
2
3
4
5
6
7
8
9
10
11

为什么 size 字段包括 header 本身?

// malloc(100) → 实际 chunk 大小:
//   header:    16 字节 (prev_size + size)
//   用户数据: 100 字节
//   对齐:       12 字节 (对齐到 16 的倍数)
//   ─────────────────
//   → 实际大小 = 128 字节 (对齐后)
//   → mchunk_size = 128 | PREV_INUSE

// malloc(100) 实际占用内存 ≥ 128 字节
// 多出的 28 字节 = header + 对齐 padding
1
2
3
4
5
6
7
8
9
10

所以面试题"malloc(100) 实际分配了多少内存"的答案是"至少 128 字节(64位系统),取决于对齐和 glibc 版本"。

# 4. 三级 Bins 分配策略

# 4.1 FastBins

Fast Bins 是速度优先的缓存——牺牲了空间合并:

Fast Bins 数组 (共 10 个, 覆盖 16~80 字节):

bins[0] → [16B chunk] → [16B chunk] → NULL     (LIFO 单链表)
bins[1] → [32B chunk] → [32B chunk] → NULL
bins[2] → [48B chunk] → NULL
...
bins[9] → [80B chunk] → [80B chunk] → [80B chunk] → NULL
1
2
3
4
5
6
7

Fast Bins 的特点:

特性 值
大小范围 16 ~ 80 字节(默认 global_max_fast = 80)
数据结构 单链表(只用 fd 指针,不用 bk)
操作方式 LIFO(后进先出)——新 free 的 chunk 插入链表头
是否合并 不合并——相邻的 fastbin chunk 保持独立
标志位维护 free 时不设 PREV_INUSE=0(为了速度)
适用 高频小对象(如临时字符串、小结构体)
// free 一个 ≤80B 的 chunk 时:
void _int_free(mstate av, mchunkptr p) {
    size_t size = chunksize(p);
    if (size <= global_max_fast) {
        // 放入 fastbin 头部 (LIFO)
        p->fd = fastbin(av, fastbin_index(size));  // 原链表头
        fastbin(av, fastbin_index(size)) = p;       // 新链表头
        return;  // ← 不合并,快速返回
    }
    // ... 否则走 slow path
}
1
2
3
4
5
6
7
8
9
10
11

# 4.2 SmallBins

Small Bins 覆盖 512 字节以内(不含 fastbin 范围)的 chunk:

Small Bins 数组 (共 62 个):

bin[2]  → 32B  chunks 双向循环链表 (精确匹配)
bin[3]  → 48B  chunks
bin[4]  → 64B  chunks
bin[5]  → 80B  chunks
...
bin[63] → 512B chunks
1
2
3
4
5
6
7
8
特性 值
大小范围 32 ~ 512 字节(步进 16B,实际 bin index 2~63)
数据结构 双向循环链表(有 fd 和 bk)
操作方式 FIFO(先进先出)——从链表头取,插入链表尾
是否合并 是——相邻空闲 chunk 在 free 时会合并

# 4.3 LargeBins

Large Bins 覆盖 > 512 字节的 chunk:

Large Bins 数组 (共 63 个):

bin[64]  →  512B ~  576B chunks (排序链表)
bin[65]  →  576B ~  640B chunks
...
bin[97]  →  3072B ~ 3584B chunks
...
bin[126] → 24576B ~ 32768B chunks
1
2
3
4
5
6
7
8
特性 值
大小范围 > 512 字节
数据结构 双向循环链表 + 按大小排序
分配算法 Best Fit(最佳适配)——找 ≥ 请求大小且最接近的
额外字段 fd_nextsize / bk_nextsize 组成按大小排序的二级链表

# 4.4 UnsortedBin

Unsorted Bin 只有 1 个(bin[1]),是所有刚释放的非 fast chunk 的第一站:

// free 时(非 fastbin chunk):
void _int_free(mstate av, mchunkptr p) {
    // ...
    // 把 p 放入 unsorted bin 头部
    p->fd = unsorted_chunks(av)->fd;
    p->bk = unsorted_chunks(av);
    // ← 放进去就完事了,不做分类
}

// malloc 时,会先遍历 unsorted bin:
// 1. 如果恰好有精确匹配 → 直接返回
// 2. 否则 → 把 unsorted chunk 分拣到对应的 small/large bin
// 3. 还没有 → 从对应的 bin 中找
1
2
3
4
5
6
7
8
9
10
11
12
13

设计用意:延迟分类——free 时不用判断该放哪个 bin(快速返回),malloc 时在需要的时候再分类(可能根本不需要分类——chunk 在 unsorted bin 中就被复用了)。

# 4.5 tcache

tcache (Thread Local Cache) 是 glibc 2.26 引入的重大性能优化:

每个线程的 tcache:

tcache[0]   →  [24B chunk] → [24B chunk]    (最多 7 个)
tcache[1]   →  [32B chunk]                   (LIFO 单链表)
tcache[2]   →  [48B chunk] → [48B chunk]
...
tcache[63]  →  [1040B chunk]
1
2
3
4
5
6
7

tcache 的核心规则:

// malloc 时: 优先从 tcache 拿 (零锁,极快)
if (tcache 有对应大小的 chunk) {
    return tcache 的链表头;
}

// free 时: 优先放入 tcache (如果没满)
if (tcache 该大小槽位未满 (默认 7 个)) {
    插入 tcache 链表头;
    return;  // ← 不进 bins!
}
1
2
3
4
5
6
7
8
9
10

tcache 带来的性能提升:

操作 无 tcache (glibc 2.25) 有 tcache (glibc 2.26+)
连续 malloc/free 相同大小 每次查 bin + 可能上锁 直接读 tcache 链表
小对象分配延迟 ~100 ns ~20 ns
多线程竞争 争 arena lock tcache 无锁

tcache 的槽位数量可通过 TCACHE_FILL_COUNT 调整(默认 7),大小上限通过 TCACHE_MAX_BINS 调整(默认 64 个槽,覆盖到 1040B)。

# 5. 双通道扩展机制

# 5.1 sbrk 小块内存的 arena 扩展

当所有 bins 都没有合适的空闲 chunk 时,glibc 尝试从 top chunk(堆顶未分配区域)切一块:

堆的布局:
┌─────┬──────┬──────┬─────────────┬─────────────────┐
│ data│ chunk│ chunk│   空闲 bins  │    top chunk     │
│/bss │  A   │  B   │   (已 free)  │  (未分配区域)     │
└─────┴──────┴──────┴─────────────┴─────────────────┘
                                         ↑
                                    program break

如果 top chunk 不够大 → 调用 sbrk(N) 把 program break 往上推
→ top chunk 变大 → 从中切一块给 malloc
1
2
3
4
5
6
7
8
9
10
// 简化的 sbrk 扩展逻辑:
if (top_chunk_size < requested_size) {
    size_t extend_size = max(requested_size + MINSIZE, MALLOC_ALIGNMENT);
    void* new_brk = sbrk(extend_size);
    // top chunk 增大 extend_size 字节
}
1
2
3
4
5
6

# 5.2 mmap 大块内存的直接映射

当请求的 chunk 大小 > MMAP_THRESHOLD(默认 128 KB)时,glibc 不走 arena→bins→top chunk 路径,而是直接调 mmap:

void* p = mmap(NULL, total_size, PROT_READ | PROT_WRITE,
               MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// chunk 的 IS_MMAPPED 标志位 = 1
1
2
3

free 时:IS_MMAPPED=1 → 直接 munmap → 虚拟空间 + 物理内存一起归还给 OS。RSS 立即下降。

# 5.3 MMAP_THRESHOLD 的动态调整

MMAP_THRESHOLD 不是固定的——glibc 会动态调整:

初始: MMAP_THRESHOLD = 128 KB
       MMAP_MAX_THRESHOLD = 默认 512 KB (可配)

如果 free 大块 mmap 时发现碎片太多:
  → MMAP_THRESHOLD 放大 (鼓励更多 chunk 走 bins)
如果 arena 碎片严重:
  → MMAP_THRESHOLD 缩小 (鼓励更多 chunk 走 mmap)
1
2
3
4
5
6
7
// 手动调整
#include <malloc.h>
mallopt(M_MMAP_THRESHOLD, 64 * 1024);   // 64KB 以上就走 mmap
mallopt(M_MMAP_MAX, 100);               // 最多 100 个 mmap 区域
1
2
3
4

# 5.4 多线程 arena 竞争与缓解

问题:多个线程同时 malloc → 都找 main_arena → 抢锁 → 阻塞。

解决:ptmalloc2 为每个线程创建独立的 arena(用 mmap 分配),一个 arena 不够时新建一个:

线程1 → main_arena (用 brk)
线程2 → arena_1 (用 mmap)
线程3 → arena_2 (用 mmap)
线程4 → arena_1 (复用已有 arena, 但需要锁)
...
1
2
3
4
5

arena 数量上限:8 * CPU 核心数(64 位系统)。

"真假"内存占用问题:当线程退出时,其 arena 不会自动释放——它仍然占据虚拟地址空间。malloc_trim(0) 可以尝试释放空闲 arena。

# 6. 碎片与合并问题

# 6.1 内部碎片 vs 外部碎片

内部碎片 (Internal Fragmentation):
  malloc(100) → 实际分配 128 字节 chunk
  → 28 字节浪费在 header + padding 上
  → 不可消除 (chunk 控制的固定开销)

外部碎片 (External Fragmentation):
  ┌────────┐ free ┌────────┐ free ┌────────┐ free ┌────────┐
  │ chunk A│ 128B │ chunk B│ 128B │ chunk C│ 512B │ chunk D│ ...
  └────────┘      └────────┘      └────────┘      └────────┘
  
  free(A) then free(C) → 得到:
  ┌────────┐ free ┌────────┐ inuse┌────────┐ free ┌────────┐
  │  空 128│      │ chunk B│ 128B │  空 512│      │ chunk D│ ...
  └────────┘      └────────┘      └────────┘      └────────┘

  malloc(600) → 没有连续 600B 的空间!尽管空闲总量 640B > 600B
  → 这是外部碎片的本质:空闲空间不连续
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 6.2 free 时的前后合并逻辑

glibc 在 free 时(非 fastbin + 非 mmap)会尝试合并相邻的空闲 chunk:

void _int_free(mstate av, mchunkptr p) {
    // 1. 检查前一个 chunk 是否空闲
    if (!prev_inuse(p)) {
        // prev_size 告诉前一个 chunk 的大小
        size_t prev_size = p->prev_size;
        // 把 p 和前一个 chunk 合并
        unlink_chunk(av, prev_chunk);     // 从 bin 中卸下前一个
        prev_chunk->size += chunksize(p); // 合并大小
        p = prev_chunk;                    // p 向前移动到合并后的 chunk
    }

    // 2. 检查后一个 chunk 是否空闲
    mchunkptr next = next_chunk(p);
    if (!prev_inuse(next_chunk(next))) {
        // 把 p 和后一个 chunk 合并
        unlink_chunk(av, next);
        p->size += chunksize(next);
    }

    // 3. 如果合并后的 chunk 紧邻 top chunk:
    if (next_chunk(p) == av->top) {
        // 直接吸收进 top chunk → 虚拟地址空间可能回收
        av->top = p;
        av->top->size += p->size;
    }

    // 4. 否则放入 unsorted bin
    else {
        // ... 放入 unsorted bin 头部
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

Fast Bins 的例外:fastbin 的 chunk 在 free 时不做合并(为了速度)。只有在 malloc 需要大 chunk 时,会"先遍历 unsorted bin,批量清理 fastbin → 合并 → 放入 unsorted bin"——这是一个延迟合并策略。

# 6.3 为什么 RSS 只涨不降

第 1 章案例的完整机理:

广告请求处理:
  malloc 2000 个小对象 (50-200B) → RSS 涨 50MB
  处理完毕 → free 2000 个对象
  → 每个 free 将 chunk 放入 tcache / fastbins (不合并)
  → RSS 不降

下一次请求:
  malloc 2000 个小对象 → tcache/fastbins 中的 chunk 足够 → 直接复用
  → RSS 不涨不降 ← 稳态

但某次请求有大对象 (如 500KB):
  malloc 500KB → 从 top chunk 切 或者 mmap
  → RSS 涨

下次请求又有 500KB:
  free 的大对象如果是 mmap → munmap → RSS 降 ✅
  如果走的是 sbrk → 进入 bins → RSS 不降 ❌
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

降 RSS 的手段:

#include <malloc.h>
malloc_trim(0);  // 尝试归还堆顶空闲空间给 OS
                 // 参数: pad (保留多少字节以防立即又要 malloc)
                 // 返回: 1 = 有归还, 0 = 没有
1
2
3
4

malloc_trim 只能归还堆顶的连续空闲区域——如果堆中间有一个未释放的 chunk 挡住了,堆顶就退不下来。

# 6.4 malloc_trim 与 malloc_stats

#include <malloc.h>

// 查看分配器内部状态
malloc_stats();
// 输出:
// Arena 0:
// system bytes     = 524288000   (从 OS 申请的)
// in use bytes     =   8388608   (实际用着的)
// max mmap regions =          8
// max mmap bytes   =  134217728

// 归还堆顶空闲空间
malloc_trim(0);   // 尝试归还所有堆顶空闲

// 查看分配器信息 (可读性更强的接口)
struct mallinfo2 info = mallinfo2();
printf("arena:    %zu\n", info.arena);    // 非 mmap 分配的总空间
printf("ordblks:  %zu\n", info.ordblks);  // 空闲 chunk 数量
printf("fordblks: %zu\n", info.fordblks); // 空闲空间总字节
printf("uordblks: %zu\n", info.uordblks); // 使用中空间总字节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 7. 内存池设计与实现

# 7.1 定长分配器

当你知道"几乎所有分配都是 64 字节"时,自定义内存池比通用 malloc 快 5~50 倍:

固定大小内存池 (64 字节):

┌─────────────────────────────────────────────────────────┐
│  pool header                                             │
│  ┌────────┬───────┬──────────────────────────────────┐  │
│  │  free  │ chunk │    预分配的 1024 个 64字节块       │  │
│  │  list  │ count │  [slot0][slot1][slot2]...[slot1023]│  │
│  └────────┴───────┴──────────────────────────────────┘  │
│      │                                                  │
└──────┼──────────────────────────────────────────────────┘
       │
       ▼
  ┌─────────┐    ┌─────────┐    ┌─────────┐
  │ slot N  │───►│ slot M │───►│ slot K  │──► NULL
  │ (空闲)  │    │ (空闲)  │    │ (空闲)  │
  └─────────┘    └─────────┘    └─────────┘
  
  空闲链表: 每个空闲 slot 的前 8 字节存放下一个空闲 slot 的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

设计要点:

  1. 一次 malloc 一大块内存,切成固定大小的 slot
  2. 空闲 slot 用侵入式链表(slot 本身存 next)——零额外开销
  3. 分配 = 从空闲链表取头节点(O(1))
  4. 释放 = 把节点放回链表头(O(1))
  5. 无锁(如果每线程一个池)或无锁 CAS(多线程)

# 7.2 完整实现与性能测试

// fixed_pool.h
#include <stddef.h>

typedef struct pool_slot {
    struct pool_slot* next;   // 空闲链表
} pool_slot_t;

typedef struct {
    pool_slot_t*  free_list;   // 空闲链表头
    char*         memory;      // 预分配的内存块基底
    size_t        slot_size;   // 每个 slot 的大小
    size_t        slot_count;  // 总 slot 数
    size_t        used_count;  // 当前已分配的 slot 数
} fixed_pool_t;

// 初始化: 预分配 slots 个大小为 slot_size 的内存块
void fixed_pool_init(fixed_pool_t* pool, size_t slot_size, size_t slots) {
    // 确保 slot 至少足够装下一个指针 (8 字节)
    if (slot_size < sizeof(pool_slot_t))
        slot_size = sizeof(pool_slot_t);

    pool->slot_size  = slot_size;
    pool->slot_count = slots;
    pool->used_count = 0;
    pool->memory     = malloc(slot_size * slots);
    pool->free_list  = NULL;

    // 把所有 slot 串成空闲链表
    for (size_t i = 0; i < slots; i++) {
        pool_slot_t* slot = (pool_slot_t*)(pool->memory + i * slot_size);
        slot->next = pool->free_list;
        pool->free_list = slot;
    }
}

// 分配: O(1)
void* fixed_pool_alloc(fixed_pool_t* pool) {
    if (!pool->free_list) return NULL;   // 池已满
    pool_slot_t* slot = pool->free_list;
    pool->free_list = slot->next;
    pool->used_count++;
    return slot;
}

// 释放: O(1)
void fixed_pool_free(fixed_pool_t* pool, void* ptr) {
    pool_slot_t* slot = (pool_slot_t*)ptr;
    slot->next = pool->free_list;
    pool->free_list = slot;
    pool->used_count--;
}

// 销毁
void fixed_pool_destroy(fixed_pool_t* pool) {
    free(pool->memory);
    pool->memory = NULL;
    pool->free_list = NULL;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

性能测试(分配+释放各 100 万次 64 字节):

// bench.c
fixed_pool_t pool;
fixed_pool_init(&pool, 64, 1000000);

// 内存池: 分配+释放 100 万次
clock_t start = clock();
for (int i = 0; i < 1000000; i++) {
    void* p = fixed_pool_alloc(&pool);
    fixed_pool_free(&pool, p);
}
clock_t end = clock();
printf("Fixed Pool:  %ld ms\n", (end - start) * 1000 / CLOCKS_PER_SEC);

// 原生 malloc/free 对比
start = clock();
for (int i = 0; i < 1000000; i++) {
    void* p = malloc(64);
    free(p);
}
end = clock();
printf("malloc/free: %ld ms\n", (end - start) * 1000 / CLOCKS_PER_SEC);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

实测结果(x86-64, glibc 2.35, 单线程):

分配器 耗时 (100万次) 相对速度
Fixed Pool ~8 ms 15× 更快
malloc/free (tcache命中) ~120 ms 1× (基准)
malloc/free (无tcache) ~350 ms 3× 更慢

为什么快这么多:

  • 内存池:两次指针读写(取链表头 + 更新链表头)≈ 4ns
  • malloc:tcache 查找 → bin 查找 → 可能 sbrk/mmap → 至少 ~30ns

# 7.3 Valgrind 内存泄漏检测原理

Valgrind 不是"运行时插桩",而是"CPU 模拟器"——它把程序的每条 x86 指令翻译成自己的中间表示(VEX IR),在翻译过程中插入影子内存检查:

你的程序 (原始 x86) → Valgrind (翻译) → VEX IR → 插入检测 → 重新生成 x86 → 执行

Valgrind 维护的元数据:
  1. Valid-Address (V) bits: 每个字节是否可访问
  2. Valid-Value (V) bits: 每个字节是否被初始化
  3. 内存分配记录: 记录每个 malloc/calloc/realloc 的调用栈
1
2
3
4
5
6

Memcheck 工具的原理(valgrind --leak-check=full):

malloc(100) → 记录: [address=0x..., size=100, stack=main:42]
             标记: 100 字节为"可访问但未初始化"

free(ptr)   → 标记: 100 字节为"不可访问"
             从记录表中移除

程序退出时:
  遍历记录表中剩余的条目 → 每个都是"泄漏" → 报告调用栈
1
2
3
4
5
6
7
8

Valgrind 的慢:大约慢 10~50 倍——因为它实质上在模拟 CPU。这是为什么--leak-check=full只用于测试,不能跑在生产环境上。

# 7.4 ASan RedZone 影子内存原理

AddressSanitizer (ASan) 采用编译期插桩(比 Valgrind 快,约 2 倍 slowdown):

编译时 ASan 做的插桩:
  每个 malloc(N) → 在实际内存周围插入 RedZone (红色区域)
  每次内存访问 → 插入影子内存检查

内存布局 (ASan):
┌──────────┬────────────────────┬──────────┐
│ RedZone  │    实际 malloc 区   │ RedZone  │
│ (32B+)   │      (N 字节)      │ (32B+)   │
│ 不可访问  │                     │ 不可访问  │
└──────────┴────────────────────┴──────────┘

影子内存 (shadow memory):
  实际内存的每个 8 字节 → 影子内存 1 字节
  影子字节 = 0x00 → 8 字节全部可访问
  影子字节 = 0xFa → 8 字节全部是 RedZone (不可访问)
  影子字节 = 0x05 → 前 5 字节可访问,后 3 字节不可访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

ASan 检测的快速访问:

// 用户代码: *ptr = 42;
// ASan 插桩后:
char* shadow_addr = (ptr >> 3) + SHADOW_OFFSET;
if (*shadow_addr != 0) {
    // 影子字节非0 → 可能越界 → 进入慢路径检查
    if (!is_accessible(ptr, sizeof(*ptr)))
        ASAN_REPORT_ERROR("heap-buffer-overflow");
}
*ptr = 42;  // 原始写入
1
2
3
4
5
6
7
8
9

ASan 的内存开销:约 2~5 倍——影子内存是实际内存的 1/8 + RedZone 增加。这也是为什么 ASan 不能长期运行生产进程。

# 8. 综合案例串讲

# 8.1 案例真相揭晓

回到第 1 章广告引擎 RSS 只涨不降的八个疑问,逐条作答:

疑问 答案
① malloc 返回的指针背后是什么? 第 3 章:chunk header(16B)+ 用户数据,prev_size 复用策略
② free 后内存去哪了? 第 4 章:tcache→fastbins→unsorted bin→small/large bins 分层缓存
③ 什么时候 sbrk vs mmap? 第 5 章:≤128KB 走 sbrk(arena),>128KB 走 mmap
④ 为什么 free 了 RSS 不降? 第 6.3:碎片+tcache/fastbins 不合并+堆中间有未释放 chunk
⑤ tcache 是什么? 第 4.5:glibc 2.26 的线程本地 LIFO 缓存,7 个/槽,无锁
⑥ Valgrind 怎么检测泄漏? 第 7.3:CPU 模拟 + 分配记录表 + 退出时报告未释放
⑦ ASan RedZone 怎么工作? 第 7.4:编译期插桩 + 影子内存 + RedZone 越界触发
⑧ 内存池比 malloc 快多少? 第 7.2:固定大小池 15× 更快,零碎片、O(1) 分配释放

第 1 章案例的完整根因链条:

广告请求: malloc 2000 小对象 (50-200B)
  → 从 tcache/fastbins/smallbins 分配 → RSS 可能涨

处理完毕: free 2000 小对象
  → 放入 tcache/fastbins (不合并)
  → RSS 不降

某次大请求: malloc 500KB
  → 如果 >MMAP_THRESHOLD → mmap → RSS 涨 500KB
  → free → munmap → RSS 降 ✅
  → 如果 ≤ MMAP_THRESHOLD → sbrk → RSS 涨
     → 堆中某处有一个 500KB 的空闲 chunk
     → 下一次 500KB 请求可能复用

但如果请求模式反复: 小对象频繁 + 偶尔大对象跨 128KB 边界
  → sbrk 堆中有擦不掉的"洞"
  → 堆顶又退不下来
  → RSS 阶梯上涨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

修复方案:

方案 A:调低 MMAP_THRESHOLD(治标)

mallopt(M_MMAP_THRESHOLD, 4096);   // 4KB 以上就走 mmap
// free 大对象 → 直接 munmap → RSS 立即下降
1
2

代价:大量 mmap/munmap 系统调用,性能下降 10~20%。

方案 B:定期 malloc_trim(缓解)

// 每处理完一批请求
if (requests_processed % 1000 == 0) {
    malloc_trim(0);   // 归还堆顶空闲空间
}
1
2
3
4

方案 C:内存池 + freed 对象复用(治本)

// 对高频对象用内存池,绕过 malloc 的碎片问题
static fixed_pool_t small_obj_pool;
fixed_pool_init(&small_obj_pool, 128, 10000);
// 这个池的内存只申请一次,零碎片
1
2
3
4

# 8.2 一次 malloc 的完整旅程

用户调用 malloc(100)
  │
  ├─ 1. tcache (如果启用)
  │      └─ 100B → 对齐到 112B → tcache[对应槽]
  │         有 → O(1) 返回 ← 快速路径,无锁!
  │         没有 ↓
  │
  ├─ 2. Fast Bins (如果 100B ≤ 80B)
  │      └─ 大小不在 fastbin 范围 → 跳过
  │
  ├─ 3. Small Bins (100 ≤ 512)
  │      └─ 查 smallbin[100/16 + 偏移] → 双向链表
  │         有 → unlink → 返回
  │         没有 ↓
  │
  ├─ 4. 合并 Fast Bins + 遍历 Unsorted Bin
  │      └─ 把所有 fastbin chunk 合并 → 加入 unsorted bin
  │         遍历 unsorted bin:
  │           精确匹配 → 返回
  │           否则 → 分拣到 small/large bin
  │
  ├─ 5. Large Bins (100 ≤ 512 不会走到这)
  │      └─ 跳过
  │
  ├─ 6. Top Chunk (所有 bin 都没合适的)
  │      └─ 从堆顶切一块 (top chunk)
  │         top chunk 够大 → 切出来 → 返回
  │         top chunk 不够
  │
  ├─ 7. 扩展堆
  │      └─ 请求大小 > MMAP_THRESHOLD (128KB)?
  │         是 → mmap(N) → 返回
  │         否 → sbrk(扩展量) → top chunk 变大 → 切 → 返回
  │
  └─ 8. 都失败了
         └─ errno = ENOMEM → 返回 NULL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 8.3 面试高频问题清单

1. malloc(100) 实际分配了多少内存?

至少 128 字节(64 位系统)。16 字节 chunk header(prev_size+size)+ 100 字节用户数据 + 12 字节对齐 padding = 128 字节。chunk 的总大小记录在 size 字段中(包含 header)。

2. Fast Bins 和 Small Bins 的核心区别?

Fast Bins:LIFO 单链表,≤80B,free 后不合并不设 PREV_INUSE=0,速度快但导致碎片。Small Bins:FIFO 双向循环链表,32~512B,free 后合并相邻空 chunk。Fast Bins 以速度换空间,Small Bins 以合并减少碎片。

3. tcache 是什么?它怎么加速 malloc/free?

glibc 2.26+ 引入的线程本地缓存。每个线程有 64 个 LIFO 槽位(覆盖 24~1040B),每槽最多 7 个 chunk。malloc 时优先从 tcache 拿(无锁),free 时优先放回 tcache。对比无 tcache 版本快 5~10 倍。

4. sbrk 和 mmap 在内存分配中分别用于什么场景?

sbrk 用于小块(≤128KB 默认),扩展堆顶(program break),无法局部释放。mmap 用于大块(>128KB),在 mmap 区域独立映射,free 时直接 munmap 归还 OS。双通道兼顾了小对象的低碎片和大对象的灵活释放。

5. free 后内存为什么不一定归还给 OS?

free 只把 chunk 放回 glibc 的 bins(tcache/fastbins/bin 链表),不调用 munmap(除非是 mmap 的大块)。bins 中的空闲 chunk 被保留以加速后续 malloc。只有通过 malloc_trim(0) 主动归还,或堆顶恰好全是空闲 chunk 时才会缩堆。

6. 什么是内部碎片和外部碎片?

内部碎片:chunk 的实际分配大小 > 请求大小(header+padding),固定开销无法消除。外部碎片:空闲空间总量足够但分散在不同位置不连续,导致无法满足大块请求。fastbins 不合并加剧外部碎片。

7. Valgrind 和 ASan 的检测原理有什么不同?

Valgrind:CPU 模拟(VEX IR 翻译),在翻译时插入内存检查,慢 10~50 倍,但不需要重新编译。ASan:编译期插桩 + 影子内存 + RedZone,慢约 2 倍,需要特殊编译,生产友好度更高。

8. 固定大小内存池为什么比 malloc 快?

内存池:O(1) 的链表操作(取头节点 + 更新 next),~4ns。malloc:至少需要查 tcache/bins + 可能的上锁 + 可能的系统调用,~30ns 起。池化还消除了碎片、避免了 page fault、缓存局部性更好。

9. Unsorted Bin 的作用?

所有非 fastbin chunk 在释放时先进入 Unsorted Bin(延迟分类)。free 时不需要判断该放入哪个具体 bin(快速返回),malloc 时再遍历 unsorted bin 做精确分类或复用。减少了 free 的延迟。

10. chunk->size 字段的最低 3 位是什么?

bit 0: PREV_INUSE(前一个 chunk 是否在使用中,决定 free 时是否向前合并)。bit 1: IS_MMAPPED(是否由 mmap 分配,决定 free 时是 munmap 还是放回 bin)。bit 2: NON_MAIN_ARENA(是否属于非主 arena)。详见第 3.3 节。

# 8.4 内存管理速查卡

诊断命令:

# 查看内存映射
pmap -x $PID                        # 进程完整地址空间
cat /proc/$PID/maps | grep heap     # 只看堆
cat /proc/$PID/smaps                # 每段的详细统计

# 内存指标
ps -o pid,vsz,rss,comm -p $PID      # VSZ + RSS
smem -p                             # PSS (按共享比例拆分)

# glibc 内部分配器状态
gdb -p $PID --batch -ex "call malloc_stats()"
gdb -p $PID --batch -ex "call malloc_info(0, stdout)"
gdb -p $PID --batch -ex "call malloc_trim(0)"

# 泄漏检测 (测试环境)
valgrind --leak-check=full --show-leak-kinds=all ./app
ASAN_OPTIONS=detect_leaks=1 ./app   # ASan 的泄漏检测

# 编译时启用 ASan
gcc -fsanitize=address -g -O2 test.c -o test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

mallopt 常用配置:

#include <malloc.h>

mallopt(M_MMAP_THRESHOLD, 64 * 1024);     // 64KB 以上用 mmap
mallopt(M_MMAP_MAX, 0);                    // 禁止 mmap
mallopt(M_TRIM_THRESHOLD, 128 * 1024);     // 128KB 空闲触发缩堆
mallopt(M_TOP_PAD, 0);                     // 缩堆不留 padding
mallopt(M_ARENA_MAX, 2);                   // 最多 2 个 arena
1
2
3
4
5
6
7

内存池核心接口模板:

typedef struct {
    void*   (*alloc)(size_t size);
    void    (*free)(void* ptr);
    void*   (*realloc)(void* ptr, size_t size);
    void    (*destroy)(void);
    size_t  used;
    size_t  capacity;
} allocator_t;

// 即插即用:把 malloc/free 替换为池子的实现
#define pool_malloc(size)  fixed_pool_alloc(&my_pool)
#define pool_free(ptr)     fixed_pool_free(&my_pool, ptr)
1
2
3
4
5
6
7
8
9
10
11
12

下一篇:19.多线程与锁机制 —— 我们已经知道"堆内存怎么被分配和回收",下一步进入并发:pthread_create 后内核做了什么?mutex 的 futex 系统调用为什么是"快锁"?自旋锁 vs 互斥锁的性能拐点在哪里?

上次更新: 2026/06/11, 09:01:44
文件IO与系统调用
未定义行为与防御

← 文件IO与系统调用 未定义行为与防御→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式