brk 与 malloc¶
是什么 / 解决什么问题¶
brk/sbrk 是传统的堆内存管理系统调用,通过移动"program break"指针扩展/缩小堆。malloc 是用户态库函数,底层结合 brk(小分配)和 mmap(大分配)管理内存。
使用模式¶
brk / sbrk¶
#include <unistd.h>
// sbrk(0): 获取当前 program break 位置
void *current_brk = sbrk(0);
// sbrk(n): 增加 n 字节,返回旧的 break 位置
void *old_brk = sbrk(4096); // 堆增长 4KB
// old_brk 到 old_brk+4096 可用
// brk(addr): 直接设置 break 到指定地址
brk(current_brk + 65536); // 堆增长 64KB
malloc 的分配策略(以 glibc 为例)¶
malloc(size):
size < M_MMAP_THRESHOLD (默认 128KB):
→ 从 brk 堆上分配(arena 中的 free list)
→ 不足时 sbrk 扩展堆
size >= M_MMAP_THRESHOLD:
→ 直接 mmap 分配
→ free 时 munmap 归还
free(ptr):
如果来自 brk 堆:
→ 放入 free list,不归还 OS
→ 只有堆顶(break 端)连续空闲时才 sbrk(-n) 收缩
如果来自 mmap:
→ 立即 munmap 归还 OS
mallopt — 调整 malloc 行为¶
#include <malloc.h>
// 调整 mmap 阈值
mallopt(M_MMAP_THRESHOLD, 64 * 1024); // 64KB 以上用 mmap
// 调整堆收缩阈值
mallopt(M_TRIM_THRESHOLD, 256 * 1024); // 堆顶空闲 256KB 时收缩
// 关闭 mmap 使用
mallopt(M_MMAP_MAX, 0); // 全部用 brk
现代 allocator¶
glibc malloc: 通用,但多线程性能一般
jemalloc: Facebook 开发,多线程友好,Redis/Rust 默认
tcmalloc: Google 开发,线程缓存减少锁竞争
mimalloc: 微软开发,极致性能
这些 allocator 都在底层使用 mmap(不依赖 brk)
行为与语义¶
brk 堆的内存模型¶
低地址
┌─────────────┐
│ 已分配 (使用中) │
├─────────────┤
│ 已分配 (已 free)│ ← 内部碎片,无法归还 OS
├─────────────┤
│ 已分配 (使用中) │
├─────────────┤ ← program break(当前堆顶)
│ │
│ 未映射空间 │
│ │
高地址
关键问题:中间的空闲块无法归还 OS。只有堆顶连续空闲时才能收缩 break。
free 不归还内存的原因¶
void *a = malloc(100);
void *b = malloc(100);
void *c = malloc(100);
free(b); // b 在中间,break 不能缩小
// RSS 不会下降!虽然 b 已 free,物理页仍属于进程
// 只有 free(c) 后堆顶空闲,且超过 trim threshold,才会 sbrk(-n)
glibc malloc 的 arena¶
多线程优化:
main arena: 使用 brk(主线程)
thread arena: 使用 mmap(非主线程)
每个线程尝试获取自己的 arena(减少锁竞争)
arena 数量上限: 8 * CPU_cores (64位)
性能考量¶
brk vs mmap 分配的权衡¶
| brk (小块) | mmap (大块) | |
|---|---|---|
| 分配速度 | 极快(用户态 free list) | 较慢(VMA 操作) |
| 释放 | 不归还 OS(复用) | 立即归还 OS |
| 碎片 | 可能累积 | 无外部碎片 |
| RSS | 可能虚高 | 精确反映使用量 |
内存碎片诊断¶
// glibc 内存统计
#include <malloc.h>
struct mallinfo2 mi = mallinfo2();
printf("arena: %zu\n", mi.arena); // brk 分配的总空间
printf("ordblks: %zu\n", mi.ordblks); // 空闲块数
printf("hblkhd: %zu\n", mi.hblkhd); // mmap 分配的总空间
printf("uordblks: %zu\n", mi.uordblks); // 正在使用的空间
printf("fordblks: %zu\n", mi.fordblks); // 空闲空间(碎片)
// fordblks 大说明碎片严重
malloc_trim — 强制归还¶
常见陷阱与 FAQ¶
1. 内存泄漏 vs RSS 不释放¶
"free 了但 RSS 不降" ≠ 内存泄漏
→ 可能是 brk 堆碎片(中间空洞无法归还)
→ malloc_trim 或使用 jemalloc 可以缓解
真正的内存泄漏: malloc 后丢失了指针,永远无法 free
2. 多线程 malloc 锁竞争¶
glibc malloc 的 arena 有锁
高并发 malloc/free → 锁竞争 → 性能下降
解决:
- 使用 jemalloc / tcmalloc
- 对象池(预分配,避免频繁 malloc)
- 每线程缓存
3. brk 和 mmap 区域不要混用¶
// 错误:对 brk 分配的内存调用 munmap
void *p = malloc(100);
munmap(p, 100); // 未定义行为!
// 错误:对 mmap 的内存调用 free(除非 allocator 自己 mmap 的)
void *p = mmap(NULL, 4096, ...);
free(p); // 崩溃或堆损坏
4. 大量小对象的内存开销¶
每个 malloc 块有元数据开销(glibc: 16 字节 header)
malloc(1) 实际消耗至少 32 字节(16 header + 16 最小块)
大量小对象 → 改用内存池或 slab allocator
观测与调试¶
# 查看进程堆使用
cat /proc/<pid>/maps | grep heap
# 55a3f4a00000-55a3f4c00000 rw-p 00000000 00:00 0 [heap]
# 大小 = 结束-起始
# 查看 malloc 统计(需要进程存活)
# 向进程发送 SIGUSR1(如果程序注册了 malloc_stats 处理)
kill -USR1 <pid>
# valgrind 检测泄漏
valgrind --leak-check=full ./app
# jemalloc 统计(如果使用 jemalloc)
MALLOC_CONF="stats_print:true" ./app
# strace 看 brk/mmap 分配
strace -e trace=brk,mmap,munmap ./app
延伸阅读¶
man 2 brk— brk/sbrk 系统调用man 3 malloc— 用户态接口man 3 mallopt— malloc 调优- glibc malloc 源码:
malloc/malloc.c - jemalloc 文档: 理解现代 allocator 设计