概述

内存是 Linux 系统中最宝贵的资源之一。理解内核如何管理内存,不仅能帮助你定位 OOM、内存泄漏等线上故障,还能在容量规划和性能调优时做出更准确的决策。从虚拟内存模型出发,覆盖 Page Cache、Swap 策略、OOM Killer 原理、cgroup v2 内存限制、slab/shmem 调优等核心主题,并附带多个生产环境实战案例。

虚拟内存模型

地址空间分层

Linux 采用虚拟内存机制,每个进程拥有独立的虚拟地址空间:

层级说明用户态可见
用户空间0x000000000000 ~ 0x00007FFFFFFFFFFF
非规范区0x0000800000000000 ~ 0xFFFF7FFFFFFFFFFF否(空洞)
内核空间0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF

64 位系统下,用户空间理论上有 128TB(47 位地址),内核空间同样 128TB。实际可用受 TASK_SIZEmm_struct 限制。

内存_zone 划分

内核将物理内存划分为多个 zone,不同 zone 有不同用途:

$ cat /proc/zoneinfo | grep -E "^Node|pages free|high|normal|DMA"
Zone用途典型场景
DMA16MB 以下,旧 ISA 设备 DMA几乎不用
DMA324GB 以下,32 位 DMA 设备老硬件
Normal4GB 以上,大多数内存分配主要使用
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_ratiodirty_ratio说明
通用服务器1020默认值,平衡
数据库服务器510减少突发 I/O 峰值
大内存机器(>128GB)15用 bytes 而非 ratio
写密集型1530允许更多缓存

对于大内存机器,建议用 _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原因
数据库服务器1swap 会导致延迟飙升
容器宿主机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 v1cgroup 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/statusVmRSS 持续增长

排查工具链

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_cacheinode 缓存vfs_cache_pressure
buffer_head块设备缓冲减少 I/O
task_struct进程描述符减少进程数
tcp_bind_bucketTCP 端口绑定减少连接数
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 限制,每一层都有对应的调优旋钮。核心要点:

  1. 理解 Page Cache 是朋友而非敌人:可用内存少不等于内存不足,CachedSReclaimable 在需要时会自动释放。
  2. Swap 不一定是坏事:低 swappiness 配合 zram 可以在内存紧张时提供缓冲。
  3. OOM Killer 是最后手段:通过 cgroup v2 的 memory.maxmemory.high 做主动控制,远比等 OOM Killer 介入更优雅。
  4. cgroup v2 是现代内存管理的基石:统一层级、细粒度统计、事件通知,是容器化环境内存控制的标准方案。
  5. 内存泄漏排查需要工具链配合:从 free/top 定位现象,到 smaps/pmap 分析分布,到 eBPF 追踪分配点,逐层深入。
  6. NUMA 感知是大内存服务器的必修课:跨节点访存延迟可达 2-3 倍,数据库等延迟敏感型应用必须做 NUMA 绑定。

内存调优的核心原则:先测量,再调优。任何参数修改前,先用 sar/vmstat/numastat 采集基线数据,修改后对比效果,避免凭直觉调参。