概述
内存是 Linux 系统中最宝贵的资源之一。理解内核如何管理内存,不仅能帮助你定位 OOM、内存泄漏等线上故障,还能在容量规划和性能调优时做出更准确的决策。从虚拟内存模型出发,覆盖 Page Cache、Swap 策略、OOM Killer 原理、cgroup v2 内存限制、slab/shmem 调优等核心主题,并附带多个生产环境实战案例。
虚拟内存模型
地址空间分层
Linux 采用虚拟内存机制,每个进程拥有独立的虚拟地址空间:
| 层级 | 说明 | 用户态可见 |
|---|---|---|
| 用户空间 | 0x000000000000 ~ 0x00007FFFFFFFFFFF | 是 |
| 非规范区 | 0x0000800000000000 ~ 0xFFFF7FFFFFFFFFFF | 否(空洞) |
| 内核空间 | 0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF | 否 |
64 位系统下,用户空间理论上有 128TB(47 位地址),内核空间同样 128TB。实际可用受 TASK_SIZE 和 mm_struct 限制。
内存_zone 划分
内核将物理内存划分为多个 zone,不同 zone 有不同用途:
$ cat /proc/zoneinfo | grep -E "^Node|pages free|high|normal|DMA"
| Zone | 用途 | 典型场景 |
|---|---|---|
| DMA | 16MB 以下,旧 ISA 设备 DMA | 几乎不用 |
| DMA32 | 4GB 以下,32 位 DMA 设备 | 老硬件 |
| Normal | 4GB 以上,大多数内存分配 | 主要使用 |
| Movable | 可迁移页面,支持内存热插拔 | 虚拟化/大页 |
当 Normal zone 内存耗尽时,内核会从 DMA32 zone 借用页面(watermark 机制),但频繁借用会导致性能下降。
页面大小与 HugePages
默认页面大小为 4KB,大页(HugePages)可以减少 TLB miss:
# 查看当前大页配置
$ cat /proc/meminfo | grep -i huge
AnonHugePages: 81920 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
# 配置 100 个 2MB 大页
$ echo 100 > /proc/sys/vm/nr_hugepages
# 透明大页(THP)状态
$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
THP 的选择建议:
| 场景 | THP 策略 | 原因 |
|---|---|---|
| 数据库(MySQL/PostgreSQL) | never | 随机访问模式,大页浪费内存 |
| 虚拟化(KVM) | always | 连续内存访问,减少 TLB miss |
| 容器运行时 | madvise | 平衡性能与内存浪费 |
| 通用 Web 服务器 | madvise | 默认推荐值 |
Page Cache 机制
工作原理
Page Cache 是内核用于缓存文件数据的内存区域。当进程 read() 文件时,内核先查 Page Cache,命中则直接返回(避免磁盘 I/O);未命中则触发磁盘读取并缓存。write() 默认先写入 Page Cache,标记为 dirty page,由 pdflush/writeback 线程异步刷盘。
$ cat /proc/meminfo | grep -E "Cached|Buffers|SwapCached|Dirty|Writeback"
Buffers: 12345 kB
Cached: 1234567 kB
SwapCached: 5678 kB
Dirty: 123 kB
Writeback: 0 kB
dirty page 刷盘参数
# dirty 占比达到内存的该百分比时,后台线程开始刷盘
$ sysctl vm.dirty_background_ratio
vm.dirty_background_ratio = 10
# dirty 占比达到内存的该百分比时,写操作被阻塞,强制刷盘
$ sysctl vm.dirty_ratio
vm.dirty_ratio = 20
# dirty 数据最大存活时间(1/100 秒)
$ sysctl vm.dirty_expire_centisecs
vm.dirty_expire_centisecs = 3000
# 唤醒刷盘线程的间隔(1/100 秒)
$ sysctl vm.dirty_writeback_centisecs
vm.dirty_writeback_centisecs = 500
不同工作负载的推荐配置:
| 场景 | dirty_background_ratio | dirty_ratio | 说明 |
|---|---|---|---|
| 通用服务器 | 10 | 20 | 默认值,平衡 |
| 数据库服务器 | 5 | 10 | 减少突发 I/O 峰值 |
| 大内存机器(>128GB) | 1 | 5 | 用 bytes 而非 ratio |
| 写密集型 | 15 | 30 | 允许更多缓存 |
对于大内存机器,建议用
_bytes替代_ratio:vm.dirty_background_bytes = 268435456 # 256MB vm.dirty_bytes = 1073741824 # 1GB
手动回收 Page Cache
# 释放 pagecache
$ echo 1 > /proc/sys/vm/drop_caches
# 释放 dentries 和 inodes
$ echo 2 > /proc/sys/vm/drop_caches
# 释放所有(pagecache + dentries + inodes)
$ echo 3 > /proc/sys/vm/drop_caches
警告:
drop_caches会导致短暂的 I/O 突增,生产环境慎用。调用前建议先执行sync。
Swap 策略
Swap 的工作方式
Swap 允许内核将不活跃的匿名页(anon pages)写入交换分区,释放物理内存给更需要的进程。Swap 的使用并不一定意味着内存不足——内核会主动将冷数据换出以提高整体效率。
swappiness 参数
# swappiness 范围 0-100(默认 60)
# 0 = 尽量不用 swap(内核 3.5+ 不完全禁用)
# 1 = 几乎不用 swap
# 60 = 默认,平衡
# 100 = 积极使用 swap
$ sysctl vm.swappiness
vm.swappiness = 60
| 场景 | swappiness | 原因 |
|---|---|---|
| 数据库服务器 | 1 | swap 会导致延迟飙升 |
| 容器宿主机 | 10 | 避免容器被 swap 拖慢 |
| 桌面系统 | 60 | 默认值 |
| 嵌入式设备 | 100 | 内存极度受限 |
vfs_cache_pressure
# 控制 inode/dentry cache 相对于 pagecache 的回收倾向
# 0 = 从不回收(不推荐)
# 100 = 默认
# >100 = 更积极回收
$ sysctl vm.vfs_cache_pressure
vm.vfs_cache_pressure = 100
Swap 状态诊断
# 查看 swap 使用情况
$ swapon --show
NAME TYPE SIZE USED PRIO
/dev/dm-1 partition 8G 1.2G -2
# 查看各进程的 swap 使用量
$ for f in /proc/*/status; do awk '/VmSwap|Name/{printf $2 " " $3 $4}END{ print ""}' "$f" 2>/dev/null; done | sort -k2 -n -r | head -20
# 查看 swap 使用趋势
$ sar -W 1 5
zram:内存压缩替代 Swap
zram 在内存中创建压缩块设备,适合内存紧张但不想用磁盘 swap 的场景:
# 创建 zram 设备
$ modprobe zram num_devices=1
$ echo lz4 > /sys/block/zram0/comp_algorithm
$ echo 4G > /sys/block/zram0/disksize
$ mkswap /dev/zram0
$ swapon -p 10 /dev/zram0
# 压缩比查看
$ cat /sys/block/zram0/mm_stat
OOM Killer 原理
触发条件
当内核无法分配内存(即使回收后仍不足)时,OOM Killer 会被触发,选择一个"最佳受害者"进程杀死以释放内存。
OOM 评分机制
每个进程有一个 oom_score(0-1000),分数越高越容易被杀:
# 查看进程的 oom_score
$ cat /proc/$PID/oom_score
# 查看 oom_score_adj(-1000 到 1000)
$ cat /proc/$PID/oom_score_adj
评分因素:
| 因素 | 影响 | 说明 |
|---|---|---|
| 内存占用 | 正相关 | 占用越多分数越高 |
| root 进程 | 负相关 | 内核倾向于保护 root 进程 |
| 子进程数量 | 负相关 | 有子进程的进程更受保护 |
| oom_score_adj | 直接调整 | -1000 完全免疫,1000 优先被杀 |
生产环境 OOM 防护
# 保护关键进程(如数据库)
$ echo -1000 > /proc/$DB_PID/oom_score_adj
# 在 systemd 中配置
cat > /etc/systemd/system/mysqld.service.d/oom.conf << 'EOF'
[Service]
OOMScoreAdjust=-1000
EOF
cgroup v2 的 OOM 控制
# 在 cgroup v2 中设置内存上限和 OOM 行为
$ echo 4G > /sys/fs/cgroup/app.memory.max
$ echo 4.5G > /sys/fs/cgroup/app.memory.high # 软限制,超过后开始回收
# 配置 OOM 时杀死整个 cgroup(而非单个进程)
$ echo 1 > /sys/fs/cgroup/app.memory.oom.group
OOM 日志分析
# 内核日志中的 OOM 记录
$ journalctl -k | grep -A 30 "Out of memory"
# 典型 OOM 日志
# Out of memory: Killed process 12345 (java) total-vm:8G, anon-rss:6G, file-rss:100M
关键字段解读:
total-vm:进程虚拟内存总量anon-rss:匿名内存(实际占用物理内存)file-rss:映射文件占用的物理内存
cgroup v2 内存控制
cgroup v1 vs v2 对比
| 特性 | cgroup v1 | cgroup v2 |
|---|---|---|
| 层级结构 | 每个子系统独立 | 统一层级 |
| 内存统计 | 粗粒度 | 细粒度(含文件/匿名页分项) |
| swap 控制 | 需额外配置 | 原生支持 |
| OOM 管理 | 有限 | 支持优先级和 group kill |
| 内核线程 | 难以控制 | 可控制 |
关键内存控制文件
# 内存硬限制
/sys/fs/cgroup/<path>/memory.max
# 内存软限制(超过后开始回收)
/sys/fs/cgroup/<path>/memory.high
# swap 限制
/sys/fs/cgroup/<path>/memory.swap.max
# 当前内存使用
/sys/fs/cgroup/<path>/memory.current
# 内存峰值
/sys/fs/cgroup/<path>/memory.peak
# 详细统计
/sys/fs/cgroup/<path>/memory.stat
# 事件通知
/sys/fs/cgroup/<path>/memory.events
容器内存限制示例
# 为应用创建 cgroup
$ mkdir /sys/fs/cgroup/myapp
# 限制内存 2G,swap 1G
$ echo 2G > /sys/fs/cgroup/myapp/memory.max
$ echo 1G > /sys/fs/cgroup/myapp/memory.swap.max
# 软限制 1.5G(超过后开始回收但不杀进程)
$ echo 1536M > /sys/fs/cgroup/myapp/memory.high
# 将进程加入 cgroup
$ echo $PID > /sys/fs/cgroup/myapp/cgroup.procs
# 查看事件
$ cat /sys/fs/cgroup/myapp/memory.events
low 0
high 0 # 超过 memory.high 的次数
max 0 # 触达 memory.max 的次数
oom 0 # OOM 事件次数
oom_kill 0 # 被 OOM 杀死的进程数
内存泄漏排查
现象识别
内存泄漏的典型表现:
- RSS 持续增长不回落
free显示可用内存持续下降- 进程被 OOM Killer 杀死后重启,然后再次增长
/proc/$PID/status中VmRSS持续增长
排查工具链
1. 基础监控
# 实时查看进程内存
$ top -p $PID
$ ps aux --sort=-%mem | head -20
# 详细进程内存映射
$ cat /proc/$PID/status | grep -E "VmRSS|VmSize|VmData|VmStk|VmExe"
# 进程内存映射
$ pmap -x $PID | tail -5
2. /proc/smaps 分析
# 查看进程的内存映射详情
$ cat /proc/$PID/smaps_rollup
Rss: 1048576 kB
Pss: 987654 kB # 比例分摊
Shared_Clean: 12345 kB
Shared_Dirty: 6789 kB
Private_Clean: 4567 kB
Private_Dirty: 1024356 kB # 关注此项是否持续增长
3. eBPF 追踪内存分配
# 使用 bcc 工具追踪 malloc 调用
$ /usr/share/bcc/tools/memleak -p $PID
# 追踪 slab 分配
$ /usr/share/bcc/tools/slabratetop
# 追踪 page 分配
$ /usr/share/bcc/tools/oomkill
4. pstack/strace 分析
# 查看进程堆栈
$ pstack $PID
# 追踪内存相关系统调用
$ strace -e trace=mmap,brk,munmap,mprotect -p $PID
Java 应用内存泄漏排查
# 查看 Java 堆使用
$ jmap -heap $PID
# 导出堆转储
$ jmap -dump:format=b,file=/tmp/heapdump.hprof $PID
# 使用 MAT 分析(离线)
$ jhat -J-Xmx4G /tmp/heapdump.hprof
Go 应用内存泄漏排查
# 查看 Go 内存统计
$ curl http://localhost:6060/debug/pprof/heap > heap.prof
# 使用 pprof 分析
$ go tool pprof heap.prof
(pprof) top 10
(pprof) list <function_name>
(pprof) web # 生成调用图
slab/shmem 调优
slab 机制
slab 是内核用于管理小对象内存分配的缓存机制。dentry cache 和 inode cache 是最大的 slab 消费者。
# 查看 slab 使用情况
$ cat /proc/meminfo | grep -E "Slab|SReclaimable|SUnreclaim"
Slab: 234567 kB
SReclaimable: 189234 kB # 可回收
SUnreclaim: 45333 kB # 不可回收
# 详细 slab 统计
$ slabtop -o | head -20
常见 slab 缓存
| 缓存名 | 说明 | 调优方向 |
|---|---|---|
| dentry | 目录项缓存 | vfs_cache_pressure |
| inode_cache | inode 缓存 | vfs_cache_pressure |
| buffer_head | 块设备缓冲 | 减少 I/O |
| task_struct | 进程描述符 | 减少进程数 |
| tcp_bind_bucket | TCP 端口绑定 | 减少连接数 |
| kmalloc-* | 通用分配 | 不需调优 |
shmem(共享内存)调优
# 查看 shmem 使用
$ cat /proc/meminfo | grep Shmem
Shmem: 45678 kB
# tmpfs 默认最大占用内存的 50%
$ mount | grep tmpfs
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
# 限制 tmpfs 大小
$ mount -o remount,size=1G /dev/shm
生产案例:dentry cache 占用过高
现象:128GB 内存的机器,free 显示可用仅剩 10GB,但所有进程 RSS 加起来不到 20GB。
排查:
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 45678901 kB
SReclaimable: 38234567 kB # 38GB 可回收 slab
$ slabtop -o | head -10
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
12345678 12000000 97% 0.19K 567890 21 2271560K dentry
3456789 3000000 86% 0.66K 98765 4 395060K inode_cache
根因:应用频繁创建/删除大量临时文件,导致 dentry cache 膨胀。
解决:
# 临时方案:手动回收
$ echo 2 > /proc/sys/vm/drop_caches
# 长期方案:调整 vfs_cache_pressure
$ sysctl -w vm.vfs_cache_pressure=200
# 或限制 dentry cache 大小(需要内核支持)
$ sysctl -w vm.dentry_cache_limit=100000000
实战案例
案例 1:Java 应用 OOM 排查
环境:4C8G 服务器运行 Java 微服务(-Xmx4G)
现象:服务运行 3 天后 OOM 重启,但 JVM 堆使用率正常(< 60%)。
排查过程:
# 1. 查看 OOM 日志
$ journalctl -k | grep "Out of memory"
# Out of memory: Killed process 12345 (java) total-vm:12G, anon-rss:6.5G
# 2. 进程 RSS 达到 6.5G,但 -Xmx4G,说明非堆内存占用 2.5G
# 3. 查看 /proc/smaps 分析
$ cat /proc/12345/smaps_rollup
Rss: 6553600 kB
Private_Dirty: 5242880 kB # 5G 私有脏页
# 4. pmap 分析
$ pmap -x 12345 | sort -k3 -n -r | head -10
# 发现大量 64MB 的 anon 映射 → 线程栈
# 5. 查看线程数
$ ls /proc/12345/task | wc -l
8200 # 8200 个线程
# 6. 每个线程栈默认 1MB,8200 线程 ≈ 8G
根因:线程池配置不当(无限制创建线程),每个线程 1MB 栈导致非堆内存膨胀。
解决方案:
- 限制线程池最大线程数
- 减小线程栈大小:
-Xss256k - 配置 OOM 保护:
OOMScoreAdjust=-500
案例 2:cgroup 内存限制导致 Redis 被杀
环境:Kubernetes Pod 中运行 Redis,limits 设置 memory: 2Gi
现象:Redis 周期性被 OOMKilled。
排查:
# 1. 查看 Kubernetes 事件
$ kubectl describe pod redis-xxx
# Last State: Terminated, Reason: OOMKilled, Exit Code: 137
# 2. Redis INFO memory
$ redis-cli INFO memory
used_memory:1.2G
used_memory_rss:1.9G # RSS 接近 2G 限制
mem_fragmentation_ratio:1.58 # 碎片率 1.58
根因:Redis 内存碎片导致 RSS 远高于 used_memory,触碰 cgroup 限制。
解决方案:
- 启用 Redis 主动碎片整理:
activedefrag yes - 调整 maxmemory 为 1.5G(留 500MB 给碎片和开销)
- 使用
jemalloc替代默认 allocator
案例 3:NUMA 架构下的内存绑定
环境:双路 CPU 服务器(2 × 32 核),256GB 内存,运行 PostgreSQL
现象:某些查询延迟不稳定,偶尔出现 10 倍以上的延迟尖峰。
排查:
# 1. 检查 NUMA 拓扑
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 ... 31
node 0 size: 128GB
node 1 cpus: 32 33 ... 63
node 1 size: 128GB
# 2. 查看 PostgreSQL 进程的 NUMA 内存分布
$ numastat -p $(pidof postgres | awk '{print $1}')
Per-node process memory usage (in MBs)
PID Node 0 Node 1 Total
12345 82000 21000 103000 # 大量内存在 Node 0
# 3. 查看 NUMA 命中/未命中统计
$ numastat
Node 0 Node 1
Hit 1234567 234567
Miss 1234 56789 # Node 1 的 miss 很高
根因:PostgreSQL 多进程模型导致内存分配不均,跨 NUMA 访问增加延迟。
解决方案:
# 方案 1:使用 numactl 绑定到特定 NUMA 节点
$ numactl --cpunodebind=0 --membind=0 postgres ...
# 方案 2:配置 interleave 模式
$ numactl --interleave=all postgres ...
# 方案 3:内核参数关闭 zone_reclaim
$ sysctl -w vm.zone_reclaim_mode=0
常用内存监控命令速查
# 系统级内存概览
$ free -h
$ vmstat 1
$ sar -r 1
# 进程级内存
$ ps aux --sort=-%mem | head
$ pmap -x $PID
$ cat /proc/$PID/status | grep -E "Vm|RSS"
# 内核内存
$ cat /proc/meminfo
$ slabtop
$ cat /proc/zoneinfo | head -40
# NUMA
$ numastat
$ numactl --hardware
# 实时追踪
$ /usr/share/bcc/tools/memleak -p $PID
$ /usr/share/bcc/tools/oomkill
$ /usr/share/bcc/tools/slabratetop
总结
Linux 内存管理是一个多层次的复杂系统,从硬件 NUMA 拓扑到内核 zone 分配器、从 Page Cache 到 Swap、从进程地址空间到 cgroup 限制,每一层都有对应的调优旋钮。核心要点:
- 理解 Page Cache 是朋友而非敌人:可用内存少不等于内存不足,
Cached和SReclaimable在需要时会自动释放。 - Swap 不一定是坏事:低 swappiness 配合 zram 可以在内存紧张时提供缓冲。
- OOM Killer 是最后手段:通过 cgroup v2 的
memory.max和memory.high做主动控制,远比等 OOM Killer 介入更优雅。 - cgroup v2 是现代内存管理的基石:统一层级、细粒度统计、事件通知,是容器化环境内存控制的标准方案。
- 内存泄漏排查需要工具链配合:从
free/top定位现象,到smaps/pmap分析分布,到 eBPF 追踪分配点,逐层深入。 - NUMA 感知是大内存服务器的必修课:跨节点访存延迟可达 2-3 倍,数据库等延迟敏感型应用必须做 NUMA 绑定。
内存调优的核心原则:先测量,再调优。任何参数修改前,先用
sar/vmstat/numastat采集基线数据,修改后对比效果,避免凭直觉调参。