内存问题: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,不看 freefree 只剩 1GB 但 available 有 12GB,系统完全健康。

内存到底被谁占了

占用方查看方式是否可回收
应用堆cat /proc/<pid>/statusVmRSS不可回收,泄漏就藏在这里
页缓存/proc/meminfoCached可回收,但回收会带来 IO
目录项与 inode 缓存/proc/meminfoSlabSReclaimable大部分可回收
脏页DirtyWriteback可回收但必须先落盘,突发写会卡住
tmpfs 与共享内存df -h /dev/shmShmem不可回收,吃的是物理内存
内核栈与网络缓冲SlabKernelStack不可回收,异常大说明有泄漏
SwapSwapCachedvmstat 的 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 抖动:延迟飙升的隐形杀手

指标健康危险
vmstatsi/so长期为 0持续非 0,说明内存不足在换页
SwapFreeSwapTotal使用率 < 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.eventsoom_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 再重启。

笔记加载中…