服务器内存占用过高排查方法

结论: 服务器内存占用过高要先分清"真占用"和"可回收缓存"——Linux 会用空闲内存做文件缓存(buff/cache),free -m 里 used 高不等于内存不够,判断依据是 available 和是否发生 swap。确认真占用后按"进程 → 堆内对象 → 趋势"三步定位,容器场景用 dmesg | grep -i oom 确认是否被 OOMKilled。

引言:内存问题最容易"看起来很严重"

内存是运维中最容易被误判的资源。刚接触 Linux 的人看到 free -m 里 16 GB 内存用了 15 GB,第一反应是"内存要爆了",于是开始杀进程、清缓存、加内存。但实际上,其中可能有 8 GB 是文件缓存(page cache),任何一个应用程序需要内存时内核都会立刻回收它——这部分内存不是占用,而是被借用的空闲。

真正需要警惕的内存占用过高有三个信号:available 长期接近于零、si/so(swap 换入换出)持续非零、进程被 OOM Killer(Out-Of-Memory Killer)杀掉。除此之外的大部分"内存高"都是正常现象,甚至是好事——缓存用得多说明磁盘 IO 被省下来了。

本文按"看懂指标 → 定位进程 → 分析堆 → 判断泄漏 → 容器场景"的顺序讲完整排查方法,命令基于常见的 Ubuntu 24.04 LTS / CentOS Stream / Rocky Linux 9 等发行版(截至 2026 年主流版本)。

第一步:看懂 free 输出,别被 used 骗了

结论:判断内存是否紧张,看 available(可用内存)和 swap 使用情况,不要看 used。used 高而 available 也高,说明内存只是被缓存借用,随时可还。

bash
# 以 MB 为单位查看内存总览
free -m

# 持续观察(每 1 秒刷新),看 available 是否在持续下降
free -m -s 1

# 最权威的原始数据
cat /proc/meminfo | egrep 'MemTotal|MemFree|MemAvailable|Buffers|Cached|SwapTotal|SwapFree|SwapCached|SReclaimable|Shmem'

free -m 输出各字段的含义:

字段含义排查时的用法
total物理内存总量基准值
used已使用(含 buff/cache 中的已用部分)单独看没有意义
free完全未使用的内存这个值低是正常的,不代表危险
shared主要是 tmpfs(/dev/shm)与共享内存占用偏高要查 tmpfs 与数据库共享内存
buff/cache块设备缓冲 + 文件页缓存可回收,内存紧张时内核自动释放
available估算的、当前可供新程序使用的内存核心判据,低于总量 10% 需要关注

一个粗略但好用的经验公式(以 /proc/meminfo 为准):

text
真正的应用占用 ≈ MemTotal - MemFree - Buffers - Cached - SReclaimable + Shmem

判断内存是否真的紧张,看这两条:

bash
# 1) 是否发生 swap 换入换出:si/so 长期非零说明物理内存真的不够了
vmstat 1 5
# 关注 si(swap in)、so(swap out) 两列,以及 free、buff、cache 三列的变化趋势

# 2) 是否已经被 OOM Killer 杀过进程(这是最硬的证据)
dmesg -T | grep -i -E 'oom|killed process'
journalctl -k --since '24 hours ago' | grep -i 'killed process'

需要说明:echo 3 > /proc/sys/vm/drop_caches 可以手动释放页缓存,但它只是验证手段,不是解决方案。释放后内存会很快被缓存重新占满,频繁手动清缓存反而会导致磁盘 IO 飙升。只有在确认缓存占用异常(如某个日志文件被反复读取撑大 page cache)时才作为临时缓解。

第二步:定位到进程,搞清楚是谁在吃内存

结论:ps aux --sort=-%mem 能快速给出 TOP 进程,但要看真实的独占内存必须用 smem 看 PSS/USS,因为共享库会被重复统计。

bash
# 按内存占用排序,看前 15 个进程
ps aux --sort=-%mem | head -15

# 只看关键列:RSS(实际驻留物理内存,KB)、VSZ(虚拟地址空间)
ps -eo pid,user,rss,vsz,comm --sort=-rss | head -15 | \
  awk '{printf "%s\t%s\t%.0fMB\t%.0fMB\t%s\n",$1,$2,$3/1024,$4/1024,$5}'

# 单进程详细内存映射(pmap -x 看每个映射段的 RSS)
pmap -x 12345 | tail -5
pmap -X 12345 | sort -k3 -rn | head -10

# 更准确的统计:PSS 把共享库按进程数分摊,USS 是进程独占部分
smem -s pss -r | head -15        # 按进程排序
smem -u -k                        # 按用户汇总
smem -P nginx -k                  # 只统计某类进程

RSS、VSZ、PSS、USS 四个指标的区别很容易混淆:

指标定义排查价值
VSZ虚拟地址空间大小(含未分配、共享库、mmap)参考价值低,Java 进程常显示几十 GB 属正常
RSS驻留物理内存,含共享库常用,但多进程共享库会被重复计算
PSS按比例分摊共享库后的内存多进程场景最准确,推荐用 smem 看
USS进程独占、不共享的部分判断"杀掉它能回收多少"最准

补充一点:/proc/PID/status 里有更细的字段,常用于脚本采集:

bash
grep -E 'VmRSS|VmSwap|VmSize|RssShmem|RssFile|RssAnon' /proc/12345/status
# RssAnon 高 = 堆/栈等匿名内存,是应用真正在用的部分
# RssFile 高 = 文件映射(程序文件、mmap 的文件、page cache 映射),可回收性更高

第三步:Java 应用的专项分析

结论:Java 进程内存高要区分"堆内(heap)"与"堆外(off-heap)"。堆内用 jstat + jmap 分析,堆外要看线程栈、直接内存(DirectByteBuffer)、元空间(Metaspace)与 JNI。

bash
PID=12345

# 1) 先看 GC 健康度:每秒采样,共 10 次
jstat -gcutil $PID 1000 10
# 关键列:E(Eden)、O(Old)、M(Metaspace)、YGC/YGCT、FGC/FGCT
# 若 O 区长期 99% 且 FGC 持续上涨 → 堆内存不足或存在泄漏

# 2) 看堆内对象分布(不触发 Full GC,开销小,可线上执行)
jmap -histo:live $PID | head -30

# 3) 堆转储(会触发 Full GC 并暂停应用,务必在低峰期执行,文件约为堆大小)
jmap -dump:format=b,file=/tmp/heap.hprof $PID
# 分析工具:Eclipse MAT、VisualVM、JProfiler

执行 jmap -dump 的注意事项:

  • 转储会让应用暂停(Stop-The-World),持续数秒到数十秒,高峰期禁止执行;
  • 生成的 .hprof 文件大小约等于当前堆使用量,4 GB 堆就是 4 GB 文件,先确认磁盘空间;
  • 建议加 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump 启动参数,让 JVM 在 OOM 时自动留存现场,避免事后没有证据。

如果 jstat 显示 Old 区不高但进程 RSS 很大,问题通常在堆外:

bash
# 线程数过多(每个线程默认栈 1 MB,1000 线程就是 1 GB)
ps -Lp $PID | wc -l

# 查看 JVM 启动参数,确认 -Xmx、-Xms、-XX:MaxDirectMemorySize、-Xss
jcmd $PID VM.flags
jinfo -flags $PID

# 直接内存与元空间使用
jcmd $PID VM.native_memory summary | head -30   # 需启动时加 -XX:NativeMemoryTracking=summary

第四步:判断是不是内存泄漏——看趋势,不看瞬时

结论:内存泄漏(Memory Leak)的唯一可靠判据是在业务量稳定的情况下,内存占用随时间单调上升且重启后重新从低位开始爬升。只看某一时刻的占用值无法区分"泄漏"与"缓存变大"。

bash
# 用 sar 回看历史趋势(需提前开启 sysstat)
sar -r -s 08:00:00 -e 20:00:00     # 看当天 kbmemused/kbmemfree 曲线
sar -r 1 60                        # 现场连续采样 60 秒

# 简易趋势采样脚本:每 30 秒记录一次 available 与 TOP 进程,跑一夜看曲线
cat > /tmp/mem-trend.sh <<'EOF'
#!/bin/bash
LOG=/tmp/mem-trend.log
while true; do
  echo "=== $(date '+%F %T') ===" >> $LOG
  grep -E 'MemAvailable|SwapFree' /proc/meminfo >> $LOG
  ps -eo pid,rss,comm --sort=-rss | head -6 >> $LOG
  sleep 30
done
EOF
chmod +x /tmp/mem-trend.sh && nohup /tmp/mem-trend.sh &

正常与泄漏的特征对比:

特征正常(缓存/业务波动)内存泄漏
available 随业务量变化高峰下降、低谷回升只降不升,呈阶梯式上升
重启进程后仍在同一水平回落到低位,然后重新爬升
Full GC 后(Java)Old 区明显下降Old 区每次 GC 后低点越来越高
缓存(buff/cache)占比高,且可回收低,匿名内存(AnonPages)占比高
swap 使用基本为 0逐渐增长且 si/so 持续非零

一个实用的辅助判据:看 /proc/meminfo 里的 AnonPages 与 SReclaimable。匿名页(Anonymous Pages)是程序真正在用的数据,不可回收;如果 AnonPages 持续增长而业务量没变,基本可以确认泄漏。

第五步:容器与 Kubernetes 场景的 OOMKilled

结论:容器里"内存占用过高"的表现往往是 Pod 被杀掉重建,状态为 OOMKilled。这类问题要同时在宿主机和 cgroup 两个层面查。

bash
# 宿主机层面:内核 OOM 日志
dmesg -T | grep -i -E 'killed process|out of memory' | tail -20

# K8s 层面:看 Pod 的 Last State 与退出原因
kubectl get pod  -o yaml | grep -A5 lastState
kubectl describe pod  | egrep -i 'OOM|Killed|Reason|memory'

# Docker 层面
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' 

# cgroup v2:查看容器内存上限、当前用量与事件计数
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/memory.events      # oom_kill 计数非零 = 确实被 OOM 杀过

容器 OOM 的一个高频坑:应用按宿主机内存算堆大小。比如宿主机 64 GB、容器限制 2 GB,JVM 未设置 -Xmx 时会按宿主机内存的 1/4 自动设堆(约 16 GB),启动后必然超出容器限制被杀。正确做法是显式设置 -Xmx,并留足堆外空间:

bash
# 容器内存限制 2 GB 时的推荐配置(堆约占 60%~70%,剩下留给元空间、线程栈、直接内存)
java -Xms1200m -Xmx1200m -XX:MaxMetaspaceSize=256m -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/data/dump -jar app.jar

# Java 8u191+ / Java 11+ 可直接让 JVM 感知容器限制
java -XX:+UseContainerSupport -XX:MaxRAMPercentage=70.0 -jar app.jar

处置手段对照表

处置手段生效速度副作用适用判断
释放页缓存 drop_caches秒级磁盘 IO 短期飙升,之后缓存重建仅作临时缓解与验证,不解决根因
重启异常进程10~60 秒业务短暂中断,现场丢失确认泄漏且暂无法修代码时的应急
限制进程内存(cgroup/systemd)秒级超限会触发应用内 OOM隔离失控进程,保护整机
调整应用内存参数(如 -Xmx)需重启需评估业务峰值配置不合理导致的占用过高
扩容加内存 / 加节点分钟~小时成本上升确认为业务真实增长
修复泄漏代码需上线需回归测试根本解法,必须做
增大 swap分钟级性能下降明显,只是延缓崩溃不推荐作为长期方案
bash
# 用 systemd 给单个服务设内存上限(超出时该服务 OOM,不影响整机)
systemctl set-property myapp.service MemoryMax=2G
systemctl show myapp.service | grep -E 'MemoryMax|MemoryHigh'

# 调整 swap 使用倾向(0=尽量不用 swap,100=积极使用;数据库服务器建议设 1~10)
sysctl vm.swappiness
sysctl -w vm.swappiness=10
echo 'vm.swappiness=10' >> /etc/sysctl.conf