结论: 服务器内存占用过高要先分清"真占用"和"可回收缓存"——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 也高,说明内存只是被缓存借用,随时可还。
# 以 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 为准):
真正的应用占用 ≈ MemTotal - MemFree - Buffers - Cached - SReclaimable + Shmem判断内存是否真的紧张,看这两条:
# 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,因为共享库会被重复统计。
# 按内存占用排序,看前 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 里有更细的字段,常用于脚本采集:
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。
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 很大,问题通常在堆外:
# 线程数过多(每个线程默认栈 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)的唯一可靠判据是在业务量稳定的情况下,内存占用随时间单调上升且重启后重新从低位开始爬升。只看某一时刻的占用值无法区分"泄漏"与"缓存变大"。
# 用 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 两个层面查。
# 宿主机层面:内核 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,并留足堆外空间:
# 容器内存限制 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 | 分钟级 | 性能下降明显,只是延缓崩溃 | 不推荐作为长期方案 |
# 用 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
企业QQ咨询




