最近帮忙看一台测试机时,看到一个很吓人的告警:某个 Java 服务在 top 里显示占了 8G。机器总共才 16G 内存,大家第一反应都是"是不是要 OOM 了"。
但再看 RES,只有 900M 左右;free -h 也没有马上见底。第一次碰到这种数字打架,我也有点懵:同一个进程,怎么一会儿像 8G,一会儿又像不到 1G?
后来才知道,VIRT 和 RES 不是两种算错了的内存,而是在回答两件不同的事。这篇不讲虚拟内存原理课,只把排查里最容易误读的几个数字捋清楚。
一. 先看清楚,8G 出现在哪一列
-
很多误会都从截图开始。
top或htop里进程的内存列不止一个,最容易被放大的通常是VIRT,有些工具也叫VSZ。它表示进程已经拿到或映射到的虚拟地址空间,并不等于这些空间此刻都占着物理内存。 -
RES对应的则更接近当前驻留在内存里的页面。换句话说,进程可以先画出一大片"可用地址",但里面有不少页还没有真正被访问;它们不会因为出现在VIRT里,就立刻把机器内存吃掉。 -
所以看到一个很大的数字,先别急着重启服务。把同一个进程的几列一起拿出来看,至少能知道自己盯着的是地址空间,还是当前驻留量。
bash
PID=12345
ps -p "$PID" -o pid,comm,vsz,rss,%mem
grep -E '^(VmSize|VmRSS|RssAnon|RssFile|RssShmem|VmSwap):' "/proc/$PID/status"
- Linux 的
/proc/PID/status里,VmSize对应虚拟内存大小,VmRSS是当前驻留集;后者还能拆成匿名页、文件映射页和共享内存页。先把这层名字对齐,后面的排查才不会越看越慌。
二. 为什么服务会先申请一大片地址
-
以 Java 为例,堆的最大值、动态库、线程栈、内存映射文件,都会出现在进程的地址空间里。Node.js、数据库和一些高性能服务也会有类似情况:它们可能提前保留地址,或者把文件映射进来,等真正需要时再逐页使用。
-
这就像你在停车场里预留了 100 个车位,不代表现在真有 100 辆车停在那里。
VIRT更像"这片地方归我用",RES才更接近"现在实际停了多少车"。所以单凭VIRT很大,不能推出服务真的泄漏了同样大的内存。 -
当然,
RES小也不代表完全没事。要是它持续上涨、机器的可用内存持续下降,或者已经开始频繁换页、使用 swap,那仍然需要继续查。关键是别用一个不合适的指标,提前给服务判死刑。 -
有时你还会发现两个进程都显示一大块
RES,它们却共享同一个动态库或文件页。把两份RES直接相加,可能又把共享部分重复算了一次。
三. 真要判断压力,别只盯着 VIRT 和 RES
- 如果告警来自"单进程到底该分多少内存",我会再看一次
PSS。它会把共享页面按参与共享的进程分摊,拿来估算一个进程对整机内存的实际份额通常更顺手。smaps_rollup是把所有映射汇总后的版本,不用面对一长串地址段。
bash
PID=12345
sudo grep -E '^(Rss|Pss|Pss_Anon|Pss_File|Pss_Shmem|Swap):' \
"/proc/$PID/smaps_rollup"
-
Linux Kernel 文档说明,
smaps_rollup会汇总各个映射的字段,并给出Pss_Anon、Pss_File、Pss_Shmem。它很适合先判断"主要是匿名内存、文件映射还是共享内存",比只看到一个总数更容易找到下一步方向。官方说明在这里。 -
接着回到整机视角。进程没有真的把内存吃满,不代表机器没有压力:页缓存、其他服务、内核开销和容器限制都可能在起作用。
MemAvailable、swap 活动和vmstat的走势,通常比某一刻的VIRT更能说明机器是否真的喘不过气。
bash
free -h
vmstat 1 5
grep -E '^(MemAvailable|SwapTotal|SwapFree):' /proc/meminfo
- 这几组信息要放在同一个时间段看。一次性的
VmRSS截图不够说明泄漏;连续几次采样中PSS、RssAnon和整机可用内存一起往坏的方向走,才更像是需要处理的增长。
四. 看到数字不对劲时,我会这样把问题缩小
-
先确认告警说的是哪个指标。是 APM 把
VIRT当"内存使用量"报出来了,还是RES/PSS确实在涨?这一问经常能省掉半天错误方向的排查。 -
如果主要涨的是
RssAnon,就更关注应用自己分配、缓存和对象释放;如果是Pss_File或RssFile,再去看最近是不是加载了大文件、索引或内存映射数据。它们不是结论,只是把"查内存"从一句大话变成更小的入口。 -
对线上服务,采样最好附带时间和进程启动时间,避免每次都拿不同代的进程对比。下面这个小命令会把最常用的几项留到一个文件里;目录请换成你们实际的临时或诊断目录。
bash
PID=12345
OUT="/tmp/memory-$PID-$(date +%Y%m%d%H%M%S).txt"
{
date
ps -p "$PID" -o pid,lstart,etime,comm,vsz,rss,%mem
grep -E '^(VmSize|VmRSS|RssAnon|RssFile|RssShmem|VmSwap):' "/proc/$PID/status"
grep -E '^(Rss|Pss|Pss_Anon|Pss_File|Pss_Shmem|Swap):' "/proc/$PID/smaps_rollup"
} | sudo tee "$OUT"
- 这个命令可能因为权限、内核版本或进程已退出而读不到部分文件,别让它静默失败。生产环境也不要高频读取完整
smaps;官方文档提到它开销更高,优先取汇总的smaps_rollup,并把频率控制在诊断需要的范围内。
五. 最后记住:大地址,不等于大压力
-
VIRT/VSZ大,首先说明进程的虚拟地址空间大;它不是"这台机器已经被这个进程占掉多少物理内存"的直接答案。RES/RSS能补上驻留量,但遇到共享页时也别急着做简单相加。 -
真要估算单进程的份额,优先补看
PSS;真要判断机器是否有压力,就把MemAvailable、swap 和一段时间内的趋势也拉进来。这样看到"服务显示 8G"时,至少知道该先问什么,而不是上来就改堆大小或重启。 -
如果最终确认的是应用真实增长,再回到对应语言的堆、缓存、连接池或文件映射去查。数字只是入口;把它读对,才不会把一次正常的地址预留,当成一场不存在的内存事故。
验证说明:文中命令面向带有
/proc的 Linux 环境。smaps_rollup的可用性与权限会受内核和运行环境影响;建议先在测试机选一个已知PID运行,再按实际监控口径配置告警。