内存管理
Linux 内存管理的核心是虚拟地址空间抽象:每个进程拥有独立的虚拟地址空间,通过页表映射到物理内存。理解分页机制、缺页中断、brk/mmap 分配方式以及 OOM Killer 的行为,是排查 Java/Go 服务内存问题的基础。
目录
| 章节 | 说明 |
|---|---|
| 虚拟地址空间布局 | 进程各段的排列与用途 |
| 分页机制 | 多级页表、TLB、缺页中断 |
| 内存分配 | brk vs mmap、malloc 内部实现 |
| SWAP 机制 | 换入换出原理与性能影响 |
| OOM Killer | 触发条件与 oom_score 调整 |
| 内存泄漏排查 | valgrind / pmap / /proc |
| Java/Go 内存实践 | JVM 堆与 Go runtime 的对应 |
虚拟地址空间布局
每个进程看到的是一片连续的虚拟地址空间,实际映射到分散的物理内存页。
| 段 | 内容 | 特点 |
|---|---|---|
| 代码段(Text) | 编译后的机器码 | 只读、可执行 |
| 数据段(Data) | 已初始化全局/静态变量 | 可读写 |
| BSS 段 | 未初始化全局变量 | 运行时清零,不占磁盘空间 |
| 堆(Heap) | malloc/new 动态分配 |
向高地址增长,brk 系统调用管理 |
| 文件映射段 | mmap、动态库 .so |
向低地址增长 |
| 栈(Stack) | 局部变量、函数调用帧 | 向低地址增长,固定大小(默认 8MB) |
| 内核空间 | 内核代码、数据结构 | 用户态不可直接访问 |
# 查看进程的内存映射布局
cat /proc/<pid>/maps
# 或使用 pmap
pmap -x <pid>
分页机制
页(Page)
内存管理的最小单位,通常为 4KB。虚拟地址 = 页号 + 页内偏移。
多级页表
32 位系统需要 100 万个页表项(4GB / 4KB),直接存储太大。Linux 使用四级页表:
虚拟地址(64位):
┌──────┬──────┬──────┬──────┬──────────────┐
│ PGD │ PUD │ PMD │ PTE │ 页内偏移 │
│ 9位 │ 9位 │ 9位 │ 9位 │ 12位 │
└──────┴──────┴──────┴──────┴──────────────┘
全局目录 上级目录 中间目录 页表项
只有实际使用的虚拟地址范围才会分配对应层级的页表,大幅节省内存。
TLB(Translation Lookaside Buffer)
MMU 中的页表高速缓存,避免每次内存访问都查多级页表。
性能影响:进程上下文切换会刷新 TLB(因为不同进程页表不同),这是上下文切换开销的主要来源之一。减少上下文切换次数可以提高 TLB 命中率。
缺页中断(Page Fault)
访问虚拟地址但对应物理页不在内存时触发:
关键特性:malloc() 返回虚拟地址后,并不立即分配物理内存,首次访问时才通过缺页中断真正分配。这是 Linux 的延迟分配(Lazy Allocation)策略。
内存分配
brk vs mmap
malloc() 内部根据大小选择不同的系统调用:
| 分配方式 | 触发条件 | 系统调用 | 释放行为 |
|---|---|---|---|
| brk | 小于 128KB | brk() 移动堆顶 |
不立即归还系统,缓存复用 |
| mmap | 大于等于 128KB | mmap() 文件映射段 |
munmap() 立即归还系统 |
# 用 strace 观察 malloc 的系统调用
strace -e trace=brk,mmap,munmap ./your_program
brk 的内存碎片问题:频繁分配/释放小对象会产生内存碎片,这是 Java 使用 GC 而非直接 free() 的原因之一。
slab 分配器
内核使用 slab 管理小于一页的内核对象(如 task_struct、inode 等),构建在伙伴系统之上,减少内存碎片。
# 查看 slab 缓存使用情况
slabtop
cat /proc/slabinfo | grep -E "dentry|inode"
SWAP 机制
当物理内存不足时,Linux 将最近最少使用的内存页换出到磁盘(Swap 分区),腾出空间。
性能影响:Swap 读写速度远低于内存(磁盘 vs DRAM),大量 Swap 活动会导致系统严重变慢(称为"内存颠簸")。
# 查看 Swap 使用情况
free -h
vmstat 1 # si(换入)、so(换出)不为 0 表示正在 Swap
# 查看各进程 Swap 使用量
for pid in /proc/[0-9]*; do
swap=$(grep VmSwap "$pid/status" 2>/dev/null | awk '{print $2}')
[ "${swap:-0}" -gt 0 ] && echo "$pid $swap kB"
done | sort -k2 -rn | head
# 临时禁用 Swap(生产谨慎)
swapoff -a
swappiness 参数:控制内核倾向于换出匿名页还是回收文件缓存(0-100,默认 60)。
# 查看当前值
cat /proc/sys/vm/swappiness
# 临时设置(重启失效)
sysctl vm.swappiness=10
OOM Killer
当系统内存耗尽且无法回收时,内核启动 OOM Killer 杀死进程。
评分机制:每个进程有 oom_score(0-1000),分数越高越容易被杀。
# 查看进程的 oom_score
cat /proc/<pid>/oom_score
# 调整 oom_adj(-17 到 15,-17 表示禁止被 OOM 杀死)
echo -16 > /proc/<pid>/oom_adj # 保护关键进程(如 sshd)
# 查看 OOM 历史
dmesg | grep -i "oom\|out of memory\|killed process"
Java 服务防 OOM 建议:
- 合理设置
-Xmx,不要超过物理内存的 70-80% - 容器环境下必须设置
-XX:+UseContainerSupport(JDK 8u191+) - 监控
oom_score,对核心服务调低oom_adj
内存泄漏排查
pmap 查看进程内存映射
# 查看进程各段内存占用
pmap -x <pid>
# 持续观察内存增长
watch -n 5 "pmap -x <pid> | tail -1"
/proc/pid/status 关键指标
cat /proc/<pid>/status | grep -E "VmRSS|VmSize|VmSwap|VmPeak"
| 字段 | 含义 |
|---|---|
| VmSize | 虚拟内存总大小(含未分配物理内存) |
| VmRSS | 常驻内存(实际占用物理内存) |
| VmSwap | 换出到 Swap 的大小 |
| VmPeak | 历史峰值虚拟内存 |
valgrind(C/C++ 内存泄漏)
valgrind --leak-check=full --show-leak-kinds=all ./your_program
Java 堆外内存泄漏
# 查看 Java 进程的本地内存(NMT)
java -XX:NativeMemoryTracking=summary -jar app.jar
jcmd <pid> VM.native_memory summary
jcmd <pid> VM.native_memory summary.diff # 对比前后差异
Java/Go 内存实践
JVM 内存模型与 Linux 对应
| JVM 区域 | Linux 对应 | 说明 |
|---|---|---|
| Java Heap(-Xmx) | mmap 匿名映射 | GC 管理,最大内存限制 |
| Metaspace | mmap | 类元数据,无上限(需设 -XX:MaxMetaspaceSize) |
| 线程栈(-Xss) | 每线程独立栈 | 默认 512KB~1MB |
| 直接内存(DirectBuffer) | mmap | NIO、Netty 使用,绕过 GC |
| JIT 编译代码缓存 | 代码段 | CodeCache,默认 240MB |
# 查看 JVM 各区域内存使用
jstat -gcutil <pid> 1000
jmap -heap <pid>
Go runtime 内存
Go 的 GC 直接管理从 OS 申请的内存,通过 mmap 大块申请,内部用 mcache/mcentral/mheap 三级结构管理。
# Go 程序内存分析
go tool pprof http://localhost:6060/debug/pprof/heap
# 查看内存分配
GODEBUG=gctrace=1 ./your_program
内核内存管理
伙伴系统(Buddy System)
内核管理物理内存页面的基础机制,以 2 的幂次(order)为单位分配连续物理页:
order 0 → 1 页(4KB)
order 1 → 2 页(8KB)
order 2 → 4 页(16KB)
...
order 11 → 2048 页(8MB)
分配时从对应 order 的空闲链表取页;释放时检查"伙伴"页是否也空闲,若是则合并为更大的块,递归向上合并。
# 查看伙伴系统各 order 的空闲页数
cat /proc/buddyinfo
# 示例:Node 0, zone Normal 1234 567 89 45 23 12 6 3 1 0 0
# 每列对应 order 0~10 的空闲块数量
Slab 分配器
伙伴系统以页为单位,而内核对象(task_struct、inode 等)通常只有几十到几百字节。Slab 分配器在伙伴系统之上,为特定大小的内核对象建立对象池,避免频繁向伙伴系统申请/归还整页内存。
核心数据结构:
分配流程:
着色区(coloring):不同 slab 中的对象起始偏移故意错开,降低 CPU Cache 行争用(Cache 抖动)。
# 查看 Slab 使用情况(Top 20 最大的缓存)
slabtop -o
# 查看特定对象的 Slab 缓存
cat /proc/slabinfo | grep -E "task_struct|dentry|inode_cache"
# 输出格式:名称 活跃对象数 总对象数 对象大小 每slab对象数 ...
Java 服务关注点:当 dentry/inode 缓存占用大量内存时,可能是文件系统操作频繁,可通过 echo 2 > /proc/sys/vm/drop_caches 回收(生产慎用)。
内存池(mempool)
内核中某些关键路径(如 I/O 路径)必须保证在内存紧张时仍能分配成功。mempool 预先分配一批对象作为保底储备:
- 正常情况下从底层分配器(如 slab)申请
- 内存紧张时从预留池中取,保证关键路径不失败
用户态无法直接使用 mempool,但理解其存在有助于解释为什么某些内核操作在内存极度紧张时仍能完成。
大页(HugePage)
标准页大小为 4KB,大量小页会导致页表项多、TLB 压力大。大页通过减少页表项提升 TLB 命中率。
两种大页机制对比:
| 机制 | 大小 | 使用方式 | 适用场景 |
|---|---|---|---|
| 标准大页(HugeTLB) | 2MB / 1GB | 显式预留,应用通过 mmap(MAP_HUGETLB) 申请 |
数据库(Oracle/MySQL)、内存密集型应用 |
| 透明大页(THP) | 2MB | 内核自动管理,对应用透明 | 通用场景,但有风险 |
透明大页(THP)的风险:
当物理内存碎片化、没有连续 2MB 空间时,内核会触发 direct compaction(直接内存规整)——扫描并迁移内存页以腾出连续空间。此过程持有粗粒度锁,耗时可达数秒,导致 CPU sys 利用率突刺。
生产环境建议:不要将 THP 设置为
always,推荐设置为madvise(让应用自行决定)或never。
# 查看 THP 当前配置
cat /sys/kernel/mm/transparent_hugepage/enabled
# 输出示例:always [madvise] never (方括号为当前值)
# 关闭 THP(立即生效)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 设置为 madvise 模式(推荐)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# 持久化(写入 /etc/rc.local 或 systemd unit)
# 查看系统中已分配的 THP 数量
grep -i hugepages /proc/meminfo
# AnonHugePages 表示 THP 占用的内存量(KB)
# 标准大页:预留 1024 个 2MB 大页(共 2GB)
echo 1024 > /proc/sys/vm/nr_hugepages
Java 服务使用大页:
# JVM 使用大页(需要系统预留了足够的大页)
java -XX:+UseLargePages -XX:LargePageSizeInBytes=2m -jar app.jar
# 查看 JVM 是否成功使用大页
java -XX:+UseLargePages -XX:+PrintFlagsFinal -version 2>&1 | grep LargePageSize
参考资料
- 《趣谈 Linux 操作系统》— 20-26 内存管理系列(刘超,极客时间)
- 《操作系统实战 45 讲》— 22-23 伙伴系统与 SLAB(LMOS,极客时间)
- 《Linux 内核技术实战课》— 18 透明大页(邵亚方,极客时间)
- 《Linux 性能优化实战》— 15-21 内存模块(倪朋飞,极客时间)
man 2 brk、man 2 mmap
评论 (0)
发表评论