内存问题:free 判读、OOM 与泄漏
内存故障有两种表现:进程被 OOM Killer 直接杀掉,或者系统开始换页导致延迟从 10ms 涨到 500ms。前者容易被发现,后者更阴险——CPU 不高、磁盘不满,只有延迟在抖。本章讲清 free 的正确读法、泄漏的量化方法、OOM 现场的取证路径。
free 的正确读法
free -h
# total used free shared buff/cache available
# Mem: 31Gi 18Gi 1.2Gi 412Mi 12Gi 12Gi
# Swap: 2.0Gi 1.1Gi 900Mi
| 列 | 含义 | 判读要点 |
|---|---|---|
total | 物理内存总量 | — |
used | 已用(不含可回收缓存) | 单看它没有意义 |
free | 完全空闲 | Linux 会把空闲内存用作缓存,free 小是正常的 |
shared | 共享内存(tmpfs 等) | 大且持续增长要查是否有进程往 /dev/shm 写 |
buff/cache | 缓冲与页缓存 | 大部分可回收,正常占用大头 |
available | 应用真正还能申请到的内存 | 唯一值得设告警的列,低于 10% 要处理 |
一句话:看 available,不看 free。free 只剩 1GB 但 available 有 12GB,系统完全健康。
内存到底被谁占了
| 占用方 | 查看方式 | 是否可回收 |
|---|---|---|
| 应用堆 | cat /proc/<pid>/status 的 VmRSS | 不可回收,泄漏就藏在这里 |
| 页缓存 | /proc/meminfo 的 Cached | 可回收,但回收会带来 IO |
| 目录项与 inode 缓存 | /proc/meminfo 的 Slab、SReclaimable | 大部分可回收 |
| 脏页 | Dirty、Writeback | 可回收但必须先落盘,突发写会卡住 |
| tmpfs 与共享内存 | df -h /dev/shm、Shmem | 不可回收,吃的是物理内存 |
| 内核栈与网络缓冲 | Slab、KernelStack | 不可回收,异常大说明有泄漏 |
| Swap | SwapCached、vmstat 的 si/so | 换出的页,访问时延迟极高 |
# 一次拿全关键内存字段
grep -E 'MemTotal|MemFree|MemAvailable|Cached|Buffers|Slab|SReclaimable|Dirty|Writeback|Shmem|SwapTotal|SwapFree' /proc/meminfo
# 页缓存被谁撑着(需要较新内核的 /proc/kpageflags 或 vmtouch,简单场景看下面两项即可)
sar -r 1 5 # 历史与实时内存使用率
sar -W 1 5 # 换页速率,非 0 即内存紧张
swap 抖动:延迟飙升的隐形杀手
| 指标 | 健康 | 危险 |
|---|---|---|
vmstat 的 si/so | 长期为 0 | 持续非 0,说明内存不足在换页 |
SwapFree 占 SwapTotal | 使用率 < 20% | 使用率 > 50% 且仍在增长 |
%wa | 低 | 与 si/so 同时升高,说明换页在打磁盘 |
| P99 延迟 | 平稳 | 阶段性尖刺,与换页时刻吻合 |
处置原则:swap 抖动比 OOM 更该优先处理。OOM 至少是快速失败,swap 会把整个服务拖成半死不活。临时动作是重启大内存进程或扩容,长期动作是核对堆上限与容器 limit 的关系。
内存泄漏的量化排查
泄漏的判断标准只有一个:RSS 随业务周期只涨不跌,而不是“内存占用高”。
# 1. 盯一个进程的 RSS 曲线(每 10 秒一次,观察 10 分钟)
while true; do
date +%T
grep -E 'VmRSS|VmSwap' /proc/<pid>/status
sleep 10
done
# 2. 看内存段分布,定位是哪一块在涨(堆、映射文件、匿名映射)
pmap -x <pid> | sort -k3 -n -r | head -20
# 3. 按实际占用排序所有进程(PSS 避免共享内存重复计算)
smem -tk -s rss | head -20
# 4. Java 应用:先看对象直方图,再决定是否 dump 堆
jmap -histo:live <pid> | head -25
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>
| 现象 | 结论 | 下一步 |
|---|---|---|
| RSS 阶梯式上涨,GC 后不回落 | 对象泄漏或缓存无上限 | 查堆内对象增长最快的类 |
| 匿名映射持续增长 | 本地内存泄漏(DirectByteBuffer、Native 调用) | 查 NMT、关闭无界直接内存缓存 |
| 映射文件数量暴涨 | 文件句柄未关或 mmap 未释放 | lsof -p <pid> | wc -l 对照 |
| Slab 持续增长不回收 | 内核对象泄漏 | slabtop 看哪个缓存最大 |
| 只有高峰期涨、低谷回落 | 正常的内存弹性,不是泄漏 | 核对峰值水位是否接近 limit |
排查顺序要记住:先用曲线证明“在泄漏”,再去 dump。没有曲线支撑就 dump 堆,只会在几百兆的文件里迷路。
OOM Killer 现场取证
# 内核日志里的 OOM 记录(时间、被杀进程、内存水位、调用栈)
dmesg -T | grep -i -E 'oom|killed process' | tail -30
journalctl -k --since "1 hour ago" | grep -i oom
# 谁最可能被杀:oom_score 越大越优先被杀
cat /proc/<pid>/oom_score
cat /proc/<pid>/oom_score_adj # -1000 到 1000,越小越不容易被杀
# 容器场景(cgroup v2)
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/memory.events # 关注 oom_kill 计数
| 证据 | 含义 |
|---|---|
Killed process <pid> (<name>) total-vm:..., anon-rss:... | 内核杀掉的进程与当时内存量 |
Out of memory: Killed process 前的 Node 0 DMA... 摘要 | 是整机 OOM 还是 cgroup OOM |
memory.events 中 oom_kill 增长 | 容器内发生 OOM,宿主可能看起来正常 |
oom_score_adj 为 -1000 | 该进程被保护,不会被杀 |
进程 RestartCount 增长且无应用错误日志 | 被 OOM 静默杀掉,去内核日志确认 |
处置动作
| 场景 | 立即动作 | 后续动作 |
|---|---|---|
| 整机 available 极低、即将 OOM | 摘流量 + 重启最大的业务进程 | 核对该进程内存上限,做容量规划 |
| 容器 OOM 反复发生 | 提高 memory limit 或加副本 | 修泄漏、限制缓存大小、调 GC |
| swap 抖动 | 关闭 swap 或降低 swappiness | 增加内存或减少常驻集 |
| 页缓存被写满导致卡顿 | 降低写入速率与脏页阈值 | 分离日志与数据盘 |
| 确认泄漏 | 保留 dump 后滚动重启 | 按 dump 定位并修复后再全量 |
调 swappiness 与脏页参数要谨慎,改错会把磁盘 IO 顶上去:
cat /proc/sys/vm/swappiness # 默认 60,数据库类服务常调到 1~10
cat /proc/sys/vm/dirty_ratio # 脏页达到该比例后应用写被阻塞
cat /proc/sys/vm/dirty_background_ratio # 后台开始回写的阈值
预防与巡检
- 告警看
MemAvailable与容器memory.current/memory.max比值,阈值 80% 预警。 - 所有缓存组件必须设上限与淘汰策略,没有上限的缓存就是定时泄漏。
- 应用启动参数显式设置堆上限,并保证堆上限 + 元空间 + 直接内存 < 容器 limit。
- 定期导出各进程 RSS 曲线,出现单调上涨趋势时提前介入,不等 OOM。
- 生产开启
-XX:+HeapDumpOnOutOfMemoryError,OOM 时自动留下堆快照。
小结:free 只信 available,buff/cache 是可回收的正常占用;内存问题分三类——OOM 被直接杀、swap 抖动拖慢延迟、RSS 只涨不跌的泄漏;先用曲线证明泄漏再用 pmap/堆直方图定位,OOM 取证去 dmesg -T 与 cgroup 的 memory.events 找 PID 与内存量;处置优先级是摘流量、扩容、留 dump 再重启。