本页目录
malloc 内部: ptmalloc2
覆盖: ptmalloc2 (glibc) → arenas/bins → mmap threshold → tcmalloc / jemalloc 对比 → 调试方法 适用: glibc 2.x
概述
glibc 的 malloc 基于 ptmalloc2 (pthread malloc v2),是 dlmalloc 的多线程改进版。核心思想:每个线程有自己的分配 arena,减少锁竞争;小分配从 bins(空闲链表)中取,大分配直接 mmap。理解内部机制对排查"malloc 慢"、"内存碎片"、"RSS 远大于实际使用量"至关重要。
Arena: 线程独立的堆
dlmalloc (单线程): 1 个堆, 1 把锁
ptmalloc2 (多线程): 多个 arena
arena (分配区):
主 arena (main_arena): 用 brk/sbrk 扩展 (位于 [heap] 区域)
子 arena: 用 mmap 分配 (位于 mmap 区域, 匿名)
线程分配时:
1. 尝试用自己上次用的 arena (TSD 缓存的)
2. 如果该 arena 被其他线程持有 → 尝试下一个 arena
3. 如果所有 arena 都有竞争 → 创建新的 arena (最多 8×cpu)
策略: 用 arena 数量换取无锁并行 → 适合多线程分配密集的程序
缺点: arena 太多 → 内存碎片 (每个 arena 有自己的 top chunk)
Bins: 空闲链表
ptmalloc2 用 bins 管理已释放的空闲块:
Fastbins (LIFO, 单链表, 不合并):
用于极小释放 (16~64 bytes, 取决于 MAX_FAST)
释放时: 入 fastbin
分配时: 从 fastbin 取 (匹配则用, 不匹配→合并)
Small bins (FIFO, 循环双向链表):
大小: 32~1024 bytes (64 bins, 每 bin 8 bytes 步进)
相邻空闲块自动合并
Large bins (FIFO + 最佳匹配):
大小: 1024+ bytes (63 bins, 每 bin 不等宽步进)
Best fit: 找 ≥size 的最小空闲块
Unsorted bin (缓存层):
新释放的块先放这里 (LIFO)
分配时遍历 → 精确匹配? 返回 : 放入对应 small/large bin
分配流程
flowchart TD
START["malloc(size)"]
START --> THRESH{"size < 128KB<br/>(M_MMAP_THRESHOLD)?"}
THRESH -->|"是 → bins 路径"| FAST{"size < MAX_FAST<br/>(~64B)?"}
THRESH -->|"否 → 大分配"| MMAP["mmap(anonymous)<br/>独立映射<br/><br/>free 时 munmap<br/>fork 友好: 不 COW"]
FAST -->|"是"| FB["查 fastbins<br/>(LIFO, 按 size index)"]
FAST -->|"否"| SMALL{"size < 1024?"}
SMALL -->|"是"| SB["查 small bins<br/>(FIFO, 双向链表)"]
SMALL -->|"否"| LARGE["① 查 unsorted bin<br/>② 匹配? 返回 : 放入 large bin<br/>③ 查 large bin (best fit)<br/>④ 都不够 → 从 top chunk 切"]
FB --> OK1["✅ 返回"]
SB --> OK2["✅ 返回"]
LARGE --> OK3["✅ 返回"]
MMAP --> OK4["✅ 返回"]
FB -.->|"未命中"| SMALL
SB -.->|"未命中"| LARGE
LARGE -.->|"top chunk 也不够"| FALLBACK["扩展 arena<br/>sbrk (主) / mmap (子)<br/>重试分配"]
FALLBACK -.->|"仍失败"| FAIL["❌ ENOMEM<br/>返回 NULL"]
classDef start fill:#e3f2fd,stroke:#1565c0
classDef decision fill:#fff3e0,stroke:#ef6c00
classDef path fill:#f3e5f5,stroke:#7b1fa2
classDef ok fill:#e8f5e9,stroke:#2e7d32
classDef fail fill:#ffebee,stroke:#c62828
class START start
class THRESH,FAST,SMALL decision
class FB,SB,LARGE,MMAP,FALLBACK path
class OK1,OK2,OK3,OK4 ok
class FAIL fail
Top Chunk
每个 arena 有一个 top chunk (堆顶的剩余空间):
所有 bin 都不够 → 从 top chunk 切 → 扩展 top chunk (sbrk/mmap)
释放的块在 top chunk 旁边? → 合并回 top chunk
mmap Threshold
// 大分配策略:
// 动态调整: 每次 free 大块 → threshold 可能减小
// → mallopt(M_MMAP_THRESHOLD, value) 可以固定
// 为什么大分配用 mmap?
// 1. 释放后立即 munmap → 还给内核 → RSS 降低
// 2. 独立映射 → 不会碎片化堆
// 3. fork 友好: 子进程改自己的 (不是 COW)
多线程陷阱
虚假共享 (False Sharing)
两个线程频繁分配/释放 → 可能共享同一个 arena
→ arena 的 mutex → 竞争 → 锁开销
缓解:
malloc_trim(0): 强制所有 arena 释放空闲空间给内核
MALLOC_ARENA_MAX: 环境变量限制最大 arena 数
内存碎片
频繁分配+释放不同大小 → bins 中的空闲块无法重用:
例: 分配 512B → free → 分配 1024B → 512B 块太小 → 从 top chunk 切
→ heap 逐渐变大但实际使用量很小 → RSS 高
检测:
malloc_stats() 或 malloc_info(0, stderr) → 看 system bytes vs in use
tcmalloc / jemalloc 对比
| ptmalloc2 (glibc) | tcmalloc (Google) | jemalloc (FreeBSD) | |
|---|---|---|---|
| 线程模型 | per-thread arena (mutex) | per-thread cache (无锁) | per-thread cache (tcache) |
| 碎片 | 中等 (bins 机制) | 低 (大小分类更细) | 低 (extent-based) |
| 大分配 | mmap, 128KB+ | mmap, 256KB+ | mmap, 2MB+ |
| 内存分析 | malloc_stats() | HeapProfiler (CPU/mem) | jeprof (CPU/mem) |
| 适合 | 通用 | 多线程服务 | 多线程服务, 低碎片 |
换用非 glibc allocator
# 运行时替换 (不重编译):
LD_PRELOAD=/usr/lib/libtcmalloc.so
LD_PRELOAD=/usr/lib/libjemalloc.so
调试与调优
# malloc 统计
MALLOC_TRACE=/tmp/mtrace.log
# 运行时参数
MALLOC_ARENA_MAX=2 MALLOC_MMAP_THRESHOLD_=65536
# valgrind 检查
# ASAN (编译时)
# 查看内存分配统计 (glibc)
#include <malloc.h>
; # XML 格式的详细统计
参考
- glibc 源码:
malloc/malloc.c(~5000行, ptmalloc2 的完整实现) - 文档:
man mallopt,man malloc_info - LWN: "The design of glibc malloc", "Hoard, tcmalloc, and jemalloc"
关键词: ptmalloc2, arena, bins, fastbin, mmap threshold, tcmalloc, jemalloc, malloc_stats, ASAN