很多时候,我们在生产环境发布服务后,习惯性地在 Pod 里敲 top,然后按内存占用排序,顺手就把锅甩给排行第一的进程。但这样做往往治标不治本——内存占用最大的进程不一定就是内存泄漏的元凶,频繁使用交换空间也未必代表内存真的吃紧。当 OOM Killer 跳出来时,真正的原因往往是内存压力随时间累积才暴露出来的。
这篇文章,我们就从 top 出发,一直深入到内核级的内存压力、换页行为和 OOM 事件,把排查思路理清楚。
别再只盯着 free
在 Linux 系统里,空闲内存实际上是“浪费”的内存。内核会主动利用未使用的 RAM 做磁盘缓存,也就是我们常看到的 buff/cache。这种做法能有效加速磁盘访问、提升整体性能。因此,当你发现“可用内存好像不够了”时,系统可能完全正常——free 和 top 显示的数字,其实很容易骗人。
很多人在排查内存问题时第一反应就是看 free 的数值,这恰恰是个大坑。free 只统计完全没有被用到的内存,而真正能再分配给程序使用的其实是 available。因为 Linux 会把空闲内存拿去做文件缓存(cache),这些缓存随时可以回收出来给应用。
top - 09:44:22 up 629 days, 19:39, 1 user, load average: 0.79, 0.62, 0.54
Tasks: 134 total, 1 running, 133 sleeping, 0 stopped, 0 zombie
%Cpu(s): 2.2 us, 3.2 sy, 0.0 ni, 94.5 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
KiB Mem : 8008272 total, 130220 free, 3184500 used, 4693552 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 4107028 avail Mem
如果 available 偏低,同时交换分区(swap)的读写持续增长,那系统十有八九正在经历真正的内存压力。接下来要做的,就是定位具体原因:
- 是某个进程因为内存泄漏或程序 Bug,占用了过多的堆内存?
- 是在高 I/O 压力下,内核来不及回收缓存?
- 还是虽然总空闲内存还有剩余,但碎片化严重,导致没法分配大块连续空间?
快速定位
监测 OOM Killer
dmesg -T | grep -E "(Out of memory|oom_kill|Killed process)" | tail -20
[Tue Feb 25 16:42:10 2025] Out of memory: Kill process 20766 (genautomata) score 34 or sacrifice child
[Tue Feb 25 16:42:10 2025] Killed process 20766 (genautomata), UID 0, total-vm:293944kB, anon-rss:286036kB, file-rss:388kB, shmem-rss:0kB
[Tue Feb 25 16:42:11 2025] [<ffffffffbc9c20cd>] oom_kill_process+0x2cd/0x490
[Tue Feb 25 16:42:11 2025] Out of memory: Kill process 3013 (test) score 34 or sacrifice child
[Tue Feb 25 16:42:11 2025] Killed process 3013 (test), UID 0, total-vm:741780kB, anon-rss:280680kB, file-rss:0kB, shmem-rss:0kB
[Tue Feb 25 16:42:12 2025] [<ffffffffbc9c20cd>] oom_kill_process+0x2cd/0x490
[Tue Feb 25 16:42:12 2025] Out of memory: Kill process 21049 (cc1plus) score 33 or sacrifice child
OOM Killer 只有在物理内存耗尽,或者内核实在回收不到足够内存时才会触发——也就是说,系统已经到了极限。
当前内存快照
free -m
total used free shared buff/cache available
Mem: 7820 3110 129 401 4580 4010
Swap: 0 0 0
重点盯住可用内存(available)与交换分区(swap)的使用情况:
- 可用内存偏低 → 预警信号
- 可用内存偏低 + 交换分区频繁使用 → 真正的内存压力
查看当前是谁在吃内存
ps aux --sort=-%mem | head -15
如果某个进程吃掉了绝大部分内存,通常逃不出这三种情况:
- 堆内存配置不当:比如 Java 程序
-Xmx 设得过大,进程被允许占用超出系统实际承载能力的内存。
- 内存泄漏:程序不断分配内存却不释放,占用随时间不断上涨,最终耗尽系统内存。
- 正常业务压力超出服务器承载上限:程序逻辑本身没问题,但业务量增长,超出了服务器内存的支撑范围。
按进程检查交换空间使用情况
for pid in /proc/[0-9]*; do
awk '/VmSwap/{print $2}' $pid/status 2>/dev/null
done | sort -rn | head -5
这条命令会打印系统上交换内存用量最高的 5 个进程(单位 KB),从大到小排序。
- 0 — 未使用交换分区(正常)
- 小规模(例如 1024–10000)——轻度交换使用
- 大型(100000+)——大量使用交换空间
- 非常大——很可能是内存压力问题
系统是否在主动换页
vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
7 0 0 121524 70172 4631872 0 0 4 57 0 0 2 3 95 0 0
0 0 0 121524 70172 4631904 0 0 0 0 40521 97413 2 3 95 0 0
0 0 0 121400 70172 4631908 0 0 0 0 34207 90644 3 3 94 0 0
2 0 0 121400 70172 4631912 0 0 0 0 38124 94556 2 4 94 0 0
1 0 0 120668 70172 4631912 0 0 0 56 35574 92296 3 4 93 0 0
交换分区的活动指标:
- si:swap in(从磁盘读入内存,单位 KB/s)
- so:swap out(从内存写回磁盘,单位 KB/s)
如果 si 和 so 持续不为 0,说明系统正在频繁换页,大概率存在内存压力。
找到真正的“内存大户”
如果 OOM Killer 已经触发,dmesg 会清楚记录被杀的进程及其内存统计信息,事后分析有明确的入口。但更多时候系统还没崩,这时我们需要找出即将惹事的进程,或是在后台缓慢、持续占内存的进程。
RSS(Resident Set Size)表示一个进程当前实际正在使用的物理内存。这才是我们真正要关注的指标。
ps aux --sort=-%mem | awk 'NR<=10 {printf "%-10s %-8s %s\n", $1, $4, $11}'
USER %MEM COMMAND
root 10.3 /xxx
lj7 5.9 vim
root 2.2 /yyy
root 1.5 /zzz
root 1.3 /CloudResetPwdUpdateAgent/depend/jre/bin/java
这样可以快速列出内存消耗最高的进程,排在最前面的自然就是首要怀疑对象。
针对性地检查该进程
有了怀疑对象,就直奔主题。
cat /proc/10623/status | grep -E "Vm(RSS|Peak|Swap|Size)"
VmPeak: 4107708 kB
VmSize: 3518108 kB
VmRSS: 830684 kB
VmSwap: 0 kB
各项指标含义:
- VmRSS — 进程实际使用的物理内存
- VmSwap — 进程被换出到交换分区的内存大小
- VmPeak — 进程内存使用峰值,用于判断内存是否在持续增长
如果 VmSwap 不断上涨,说明这个进程正在加剧系统内存压力。
检查内存映射(用于排查内存泄漏)
pmap -x <PID> | sort -k3 -rn | head -20
这会展示进程的内存分配区域。较大的匿名映射或持续增长的内存段可能意味着:
诊断内存泄漏
如果一个进程的内存使用量只涨不跌,那多半存在内存泄漏。正常的应用程序在负载上升时内存增长,但压力降下来后也应该释放内存。
跟踪增长情况
确认内存泄漏最可靠的方法是持续观察进程。单次快照说明不了问题,我们要看内存使用量是:
这就需要长时间观测,可以写个简单脚本:
PID=12345
while true; do
echo "$(date +%T) RSS: $(awk '/VmRSS/{print $2}' /proc/$PID/status) kB"
sleep 5
done
# Output:
03:21:00 RSS: 524288 kB
03:21:05 RSS: 527360 kB
03:21:10 RSS: 531456 kB <-- growing each sample, bad sign
或者把数据记录到文件,再计算增量:
while true; do
echo "$(date +%s) $(awk '/VmRSS/{print $2}' /proc/$PID/status)"
sleep 10
done >> /tmp/rss_log.txt &
awk 'NR>1 {print $1, $2, $2-prev} {prev=$2}' /tmp/rss_log.txt | tail -20
使用 valgrind 和 heaptrack 进行内存分析
对于 C/C++ 程序,valgrind --tool=massif 能给出详细的内存分配调用树。但生产环境很难承受 Valgrind 带来的巨大性能开销(可能下降 10-50 倍),这时更轻量的 heaptrack 就登场了,它还能直接 attach 到正在运行的进程上。
# Attach to running process (requires heaptrack installed)
heaptrack --pid <PID>
# ... let it run, Ctrl+C when done
# Analyze the output
heaptrack_print heaptrack.<processname>.<pid>.gz | head -50
PEAK consumption: 6.25 GB
top 5 allocations by peak contribution:
peak: 4.1 GB, allocations: 892014
0x7f3a2b... malloc (libc.so)
0x55f123... build_cache_entry (cache.cpp:247) <-- likely culprit
0x55f089... process_request (handler.cpp:112)
# For Java/JVM processes, use jmap
jmap -heap <PID> # heap summary
jmap -histo:live <PID> # object histogram (triggers GC first)
理解 /proc/meminfo
/proc/meminfo 暴露了内核的原始内存使用数据。我们常用的内存管理工具(比如 free),底层都从这里读取信息。
cat /proc/meminfo
MemTotal: 8008272 kB
MemFree: 126884 kB
MemAvailable: 4092284 kB
Buffers: 65184 kB
Cached: 4395652 kB
SwapCached: 0 kB
Active: 5130280 kB
Inactive: 2162340 kB
Active(anon): 3096104 kB
Inactive(anon): 146512 kB
Active(file): 2034176 kB
Inactive(file): 2015828 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 13248 kB
Writeback: 0 kB
AnonPages: 2831788 kB
Mapped: 83476 kB
Shmem: 410828 kB
Slab: 267732 kB
SReclaimable: 221312 kB
SUnreclaim: 46420 kB
KernelStack: 19968 kB
PageTables: 19312 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 4004136 kB
Committed_AS: 11706324 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 29844 kB
VmallocChunk: 34359609340 kB
Percpu: 656 kB
HardwareCorrupted: 0 kB
AnonHugePages: 641024 kB
CmaTotal: 0 kB
CmaFree: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
DirectMap4k: 105984 kB
DirectMap2M: 6184960 kB
DirectMap1G: 4194304 kB
看起来字段很多,其实真正关键的只有几个:
- MemAvailable:系统真正可用的内存
- Committed_AS / CommitLimit:内存分配失败的风险程度
- SUnreclaim:可能存在内核内存泄漏的迹象
- Cached:正常且健康的缓存,完全不用担心
以上便是 Linux 内存排查的核心步骤。如果你有更多实战心得,欢迎到云栈社区交流讨论。