跳转至

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 — 强制归还

// 尝试归还 brk 堆顶的空闲内存
malloc_trim(0);
// 参数是保留的 pad 大小

// 某些场景定期调用(如 Go runtime 的 scavenger)

常见陷阱与 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 设计