Linux 服务器内存占用异常排查:一步步定位问题进程

服务器内存不断增长,是每个运维和后端工程师都会遇到的问题。本文记录一次完整的内存排查过程,从发现问题到定位根因。

top 与 ps 基础命令

top 命令

1
top

输出:

1
2
3
4
5
6
7
8
9
10
top - 10:30:00 up 50 days,  3:22,  2 users,  load average: 0.52, 0.48, 0.45
Tasks: 203 total, 1 running, 202 sleeping, 0 stopped, 0 zombie
%Cpu(s): 5.2 us, 1.3 sy, 0.0 ni, 93.5 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 15872.8 total, 2048.4 free, 11234.5 used, 2589.9 buff/cache
MiB Swap: 2048.0 total, 1524.2 free, 523.8 used. 2598.3 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12345 java 20 0 8234567 2.5g 123456 S 25.0 16.5 5:23.45 java
23456 python 20 0 567890 234567 56789 S 0.5 1.5 0:12.34 python
...

关键字段:

  • VIRT:虚拟内存总量
  • RES:实际占用的物理内存(常驻内存 RSS)
  • SHR:共享内存
  • %MEM:内存占用百分比

ps 命令

1
2
3
4
5
6
7
8
9
10
11
# 查看内存占用最高的进程
ps aux --sort=-%mem | head -20

# 查看特定用户的进程
ps -u username -o pid,rss,pmem,comm

# 查看详细内存信息
ps -eo pid,vsz,rss,pmem,comm --sort=-rss | head -20

# 进程树(按内存排序)
ps auxf --sort=-rss | head -30

常用组合

1
2
3
4
5
6
7
8
# 实时监控内存
watch -n 1 'ps aux --sort=-%mem | head -15'

# 查看 Java 进程内存
ps -eo pid,rss,pmem,comm | grep java

# 查看所有进程的内存总和
ps aux | awk '{sum+=$6} END {print sum/1024/1024 " GB"}'

/proc 内存分析

Linux 的 /proc 目录包含内核和进程的信息。

查看进程内存映射

1
2
3
4
5
# 进程内存映射
cat /proc/<pid>/maps

# 查看内存区域
cat /proc/<pid>/smaps

输出示例:

1
2
3
00400000-00452000 r-xp 00000000 fd:00 123456 /path/to/executable
00651000-00652000 r--p 00051000 fd:00 123456 /path/to/executable
...

查看内存详情

1
2
3
4
5
6
7
8
9
10
# 内存状态(单位 pages)
cat /proc/<pid>/status | grep -E "VmSize|VmRSS|VmData|VmStk|VmExe"

# 输出:
# VmPeak: 8234567 kB # 峰值虚拟内存
# VmSize: 8234567 kB # 当前虚拟内存
# VmRSS: 2621440 kB # 实际物理内存
# VmData: 2097152 kB # 数据段大小
# VmStk: 8192 kB # 堆栈大小
# VmExe: 4096 kB # 代码段大小

查看 OOM 杀掉的进程

1
2
3
4
5
6
7
8
9
# 系统日志中的 OOM
dmesg | grep -i "out of memory"
dmesg | grep -i "killed process"

# /var/log/messages
grep -i memory /var/log/messages | tail -20

# OOM killer 日志
cat /var/log/kern.log | grep oom

内存类型解析

Linux 内存分为多种类型,理解它们有助于定位问题。

内存类型说明

类型 说明 正常值
VIRT 虚拟内存 可大于物理内存
RES 常驻内存 应接近实际使用
SHR 共享内存 多进程共享库
Data 数据段 堆 + 栈 + 数据

查看内存分布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 内存总量和空闲
free -h

# 详细内存信息
cat /proc/meminfo

# 关键指标:
# MemTotal: 总物理内存
# MemFree: 完全空闲
# MemAvailable: 可用内存(包括缓存)
# Buffers: 块设备缓存
# Cached: 文件系统缓存
# SwapTotal: swap 总大小
# SwapFree: swap 空闲

buff/cache 区别

1
2
3
4
5
6
7
8
9
10
# Buffers: 块设备(磁盘块)的缓存
# - 存储磁盘块的元数据
# - 读写磁盘块

# Cached: 文件系统缓存
# - 存储文件内容
# - 读文件时缓存内容,写文件时临时存储

# 释放缓存(需要 root)
echo 3 > /proc/sys/vm/drop_caches

判断是否真的内存不足

1
2
3
4
5
6
7
8
# 如果 available 远大于 free,说明内存被缓存占用
# 这是正常的,不需要担心
free -h
# total used free shared buff/cache available
# Mem: 16Gi 8.5Gi 2.1Gi 156Mi 5.4Gi 7.2Gi
# Swap: 2.0Gi 500Mi 1.5Gi

# 如果 available 接近 free,说明真的内存不足

缓存与缓冲区别

page cache(页缓存)

页缓存是内存中存储文件数据的地方:

1
2
3
4
5
# 查看页缓存
cat /proc/meminfo | grep -E "^Cached|^Buffers"

# Cached: 5242880 kB # 文件缓存
# Buffers: 524288 kB # 块设备缓存

swap 使用分析

1
2
3
4
5
6
7
8
9
# 查看 swap 使用
swapon -s

# 或
cat /proc/swaps

# 输出:
# Filename Type Size Used Priority
# /dev/sda2 partition 2097152 523456 -1

监控 swap 活动

1
2
3
4
5
6
7
8
9
10
# 查看 swap in/out
vmstat 1

# 输出:
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa
# 1 0 0 2097152 0 5242880 0 0 0 0 50 100 5 2 93 0

# si: 每秒从 swap 读入内存 (swap in)
# so: 每秒从内存写入 swap (swap out)

判断内存泄漏

1
2
3
4
5
6
7
8
9
# 观察内存使用趋势
while true; do
date
ps aux --sort=-rss | head -5
sleep 60
done >> memory.log

# 检查进程内存是否持续增长
# 如果 RES 不断增长且不回落,可能是内存泄漏

实战:定位内存泄漏进程

步骤1:确认问题

1
2
3
4
5
6
7
8
# 查看整体内存
free -h
# total used free shared buff/cache available
# Mem: 62Gi 58Gi 1.2Gi 200Mi 2.6Gi 2.8Gi
# Swap: 8.0Gi 7.5Gi 500Mi

# 问题确认:available 只有 2.8G,swap 使用 7.5G
# 内存严重不足

步骤2:找出占用最高的进程

1
ps aux --sort=-%mem | head -20
1
2
3
4
5
USER       PID    %MEM   RSS
mysql 12345 25.0 16.5G mysqld
java 23456 15.0 10.2G java
python 34567 8.0 5.2G python
...

步骤3:分析具体进程

1
2
3
4
5
6
7
8
# 查看 Java 进程的内存映射
cat /proc/23456/smaps | grep -A 2 Heap

# 或者使用 pmap
pmap -x 23456 | sort -k3 -n | tail -20

# jmap 导出 heap dump(需要 JDK)
jmap -dump:format=b,file=heap.hprof 23456

步骤4:如果是 MySQL

1
2
3
4
5
6
7
8
9
# 查看 MySQL 内存使用
mysql -e "SHOW PROCESSLIST;"
mysql -e "SHOW ENGINE INNODB STATUS\G"

# 查看缓冲池大小
mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"

# 查看临时表内存使用
mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%';"

步骤5:如果是 Java

1
2
3
4
5
6
7
8
9
10
11
12
# 查看 JVM 堆外内存
jstat -gc 23456

# 查看 JVM 参数
jcmd 23456 VM.flags

# 常见问题:Metaspace 过大
# 解决:添加 -XX:MaxMetaspaceSize=512m

# Direct Buffer 泄漏
# 查看 NIO buffer
cat /proc/23456/smaps | grep -i direct

步骤6:如果是 Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Python 内存泄漏
pip install objgraph

python -c "
import objgraph
import tracemalloc
tracemalloc.start()

# 运行代码...

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print('Top 10:')
for stat in top_stats[:10]:
print(stat)
"

步骤7:定位泄漏代码

1
2
3
4
5
6
# 使用 strace 追踪内存分配(慎用,会很慢)
strace -p 23456 -e brk -f

# 或使用 perf
perf record -g -p 23456 -- sleep 30
perf report

常见问题与解决

问题1:Java 堆外内存泄漏

1
2
3
4
5
6
# JVM 参数限制
java -XX:MaxRAMPercentage=75 \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=256m \
-XX:MaxDirectMemorySize=512m \
-jar app.jar

问题2:Nginx 内存泄漏

1
2
3
4
5
6
7
# 限制 worker 进程
worker_processes auto;
worker_rlimit_nofile 65535;

events {
worker_connections 10240;
}

问题3:Python 内存持续增长

1
2
3
4
5
6
7
8
9
10
11
# 使用弱引用
import weakref

cache = weakref.WeakValueDictionary()

# 或定时清理
import gc

def clear_memory():
gc.collect()
# 清理后手动执行

问题4:MySQL 临时表占用过多内存

1
2
3
-- 限制临时表大小
SET GLOBAL tmp_table_size = 64M;
SET GLOBAL max_heap_table_size = 64M;

总结

Linux 内存排查流程:

  1. 观察整体free -htop
  2. 定位进程ps aux --sort=-%mem | head
  3. 分析进程/proc/<pid>/statuspmap
  4. 判断类型:堆、堆外、缓存、swap
  5. 定位根因:代码、配置、JVM 参数
  6. 解决:修复代码或调整配置

关键命令速查:

命令 用途
free -h 内存概览
top 实时监控
ps aux --sort=-%mem 按内存排序进程
cat /proc/meminfo 详细内存信息
vmstat 1 内存和 swap 活动
pmap -x <pid> 进程内存映射
dmesg | grep -i oom OOM 日志