Linux内存观测:VIRT、RES、SHR三列详解与误判排查 top 命令的输出界面很多人天天看但真正能把 VIRT、RES、SHR 这三列讲清楚的人并不多。我见过不少排查内存问题的场景有人盯着 RES 一路飙升就断定内存泄漏结果重启完发现是页缓存作祟也有人看到 VIRT 几十个 G 直接吓一跳其实进程根本没占那么多物理内存。这三个参数是 Linux 内存观测里最基础也最容易被误读的部分搞不清它们的边界后面所有关于内存的分析都是空中楼阁。这篇内容面向的是需要经常登录服务器看进程状态的后端开发、运维、测试以及正在准备系统相关面试的朋友。我会把 VIRT、RES、SHR 各自的统计口径、它们之间的包含关系、以及在实际排查中怎么组合着看一层层拆开讲。重点不是背定义而是让你看到某个数字异常时能立刻判断它到底意味着什么、下一步该查哪里。文中涉及的操作和命令都可以直接在自己的机器上复现建议边看边敲。1. 先把 top 里这三列放回内存模型里理解1.1 为什么单看一个数字永远得不出结论很多人第一次看 top 会有一个直觉内存占用就是某一列数字越大越危险。这个直觉之所以错是因为 Linux 的进程内存本身就不是一个单一维度的量。一个进程从操作系统那里要到的地址空间、它实际摸过的物理页、以及这些物理页里有多少是和其他进程共享的是三件不同的事。VIRT 描述的是第一件RES 描述的是第二件SHR 是第二件里的一个子集。打个生活化的比方。VIRT 像是你向公司申请的办公面积预算预算批了多少不代表你真的用满了RES 是你实际摆上桌子、堆了东西的面积SHR 则是你和隔壁工位共用的那部分区域比如公共打印区、共享会议室。预算可以申请得很大实际用的可能很小实际用的里面又有一部分其实不是你独占的。你只报预算面积会吓到人只报独占面积又会漏掉共享部分带来的整体压力所以必须三个一起看。从内核角度看这三列分别对应task-mm里的不同统计字段。top 读取的是/proc/[pid]/statm和/proc/[pid]/status里的数据其中 VmSize 对应 VIRTVmRSS 对应 RESRssFile 与 RssShmem 之和大致对应 SHR。理解这个数据来源很重要因为当 top 的数字和你用其他工具看到的对不上时往往就是统计口径和采样时机的问题而不是工具坏了。1.2 虚拟地址空间到底虚在哪VIRT 的全称是 Virtual Memory Size中文一般叫虚拟内存大小或虚拟地址空间。它统计的是进程当前映射的虚拟地址总字节数包括代码段、数据段、堆、栈、所有 mmap 出来的区域、共享库的映射甚至包括已经申请但还没真正访问的地址范围。关键在于虚拟两个字。进程向内核申请内存时内核通常只是给它记一笔账说这块地址归你了并不会立刻分配物理页。真正分配物理页发生在进程第一次读写这块地址的时候这叫按需分页demand paging。所以一个进程 VIRT 显示 20G完全可能实际只用了 200M 物理内存。Java 应用、Go 应用、数据库这类会预留大块地址空间的程序VIRT 动辄几十上百 G 是常态看到这个数字不必惊慌。VIRT 里还包含大量共享库的映射。比如一个进程链接了 glibc、libssl、libstdc 等一堆动态库每个库都会被映射进它的虚拟地址空间这些都会计入 VIRT。多个进程链接同样的库各自的 VIRT 里都会算一遍但物理内存里这些库的代码页只有一份由所有进程共享。这就是为什么 VIRT 的加总毫无意义你把所有进程的 VIRT 加起来能轻松超过物理内存几十倍这并不代表内存不够用。还有一个容易忽略的点线程栈。每个线程默认会预留一定大小的栈空间常见 8M这些预留也计入 VIRT。一个开了几百线程的进程光线程栈的虚拟占用就能有好几个 G。所以看到 VIRT 特别大时先想想这个进程是不是线程数很多、是不是用了大堆预留或者大块 mmap。1.3 RES 才是真金白银占着的物理内存RES 全称 Resident Set Size中文叫常驻内存集大小指的是进程当前实际驻留在物理内存中的页的总量。注意这里的关键词是驻留——这些页此刻确实在 RAM 里没有被换出到交换分区。你摸过的每一页、还没被回收的每一页都算在 RES 里。RES 是排查内存问题最常看的指标因为它最接近这个进程到底吃了多少内存的直觉。但即便是 RES也有几个坑要提前说清楚。第一RES 包含共享页。一个进程 mmap 了一个 1G 的文件并读了一遍这 1G 的页缓存会算进它的 RES但这些页其实是页缓存系统内存紧张时可以直接丢弃不算真正的占用。第二RES 会随着内存回收波动同一个进程在空闲时和压力下的 RES 可能差很多单次采样说明不了趋势。第三RES 不包含已经被换出到交换分区的部分所以开了交换分区的机器上RES 之和可能明显小于进程实际用过的内存。我个人的经验是判断一个进程是否真的在持续吃内存不要看单点 RES而要看一段时间内 RES 的增长曲线并且结合 SHR 和系统整体可用内存一起判断。如果 RES 涨、SHR 不涨、系统可用内存同步下降那大概率是进程自己在堆上分配了东西如果 RES 涨但 SHR 也涨、系统可用内存没怎么变那可能只是它读了更多文件页缓存变多了而已。1.4 SHR 是 RES 里和别人分着用的那部分SHR 全称 Shared Memory指的是 RES 中与其他进程共享的那部分页。它不是一个独立于 RES 的量而是 RES 的子集。换句话说SHR 永远小于等于 RES正常情况下它告诉你我常驻的物理页里有多少是可能和别人共用的。哪些东西会算进 SHR典型的有动态链接库的代码段多个进程链接同一个库时共享同一份物理页、通过 mmap 映射的共享文件页、System V 或 POSIX 共享内存段、以及 fork 之后父子进程之间写时复制copy-on-write尚未发生写操作的页。这些页的特点是物理内存里只有一份但会被多个进程的 RES 各算一次。这就引出一个非常重要的推论把所有进程的 RES 加起来会明显大于系统实际使用的物理内存因为共享页被重复计算了。同理把所有进程的 SHR 加起来也没有直接意义因为同一份共享页会被每个使用者都记一笔。真正想看系统整体内存用了多少应该看free命令的 used 列或者/proc/meminfo里的 MemTotal - MemAvailable而不是去加进程的 RES。理解了 SHR 是 RES 的子集、且共享页会被重复计数这两点很多内存对不上账的困惑就迎刃而解了。下面这张表先把三者的核心差异摆出来方便随时对照。参数全称统计对象是否含共享加总是否有意义典型误读VIRTVirtual Memory Size虚拟地址空间映射总量含共享库映射等无意义可远超物理内存以为进程真占了这么多RESResident Set Size实际驻留物理内存的页含共享页计入偏大共享页重复计以为加总等于系统用量SHRShared MemoryRES 中共享的那部分本身就是共享无意义重复计更严重以为它是独立于 RES 的量2. 三个数字之间的包含关系与常见误判2.1 VIRT ≥ RES ≥ SHR 这条链为什么成立正常情况下VIRT、RES、SHR 满足 VIRT ≥ RES ≥ SHR 的关系。VIRT 是地址空间总量RES 是其中真正驻留物理内存的部分所以 RES 不可能超过 VIRTSHR 是 RES 的子集所以 SHR 不可能超过 RES。这条链是理解三者关系的基础一旦你看到某个进程 SHR 大于 RES那基本可以断定是采样时机导致的瞬时不一致或者统计口径有特殊情况值得进一步查。为什么会出现瞬时不一致因为 top 读取/proc/[pid]/statm时各个字段并不是在同一个原子操作里一次性读出来的内核在填充这些字段的过程中进程的内存状态可能已经变了。比如你读 VmRSS 的时候是 100M读 RssFile 的时候进程刚好又 mmap 了一个文件两个数字拼起来就可能出现 SHR 略大于 RES 的假象。这种情况在高并发、内存频繁变动的进程上偶尔能碰到不必大惊小怪多采样几次取稳定值即可。还有一种情况是内核统计口径的细微差异。不同内核版本对某些特殊映射比如大页、设备映射、某些特殊文件系统的归类可能不同导致 SHR 的统计范围略有出入。这类差异通常很小不影响整体判断但如果你的监控系统对 SHR 做了严格告警就要留意内核版本升级带来的基线漂移。2.2 RES 涨了就是内存泄漏这个结论错在哪这是我在实际排查中最常纠正的一个误判。RES 上涨有很多种原因内存泄漏只是其中之一而且往往不是最常见的那种。下面几种情况都会让 RES 明显上涨但都不是泄漏。第一种是页缓存计入。进程读了一个大文件内核会把文件页缓存起来这些页会计入进程的 RES通过 RssFile。这种增长是良性的系统内存紧张时可以直接回收不会导致 OOM。判断方法是看 SHR 是否同步上涨、以及/proc/[pid]/smaps里 RssFile 的占比。第二种是内存分配器的缓存行为。glibc 的 malloc 在 free 之后不一定立刻把内存还给操作系统而是留在自己的 arena 里备用这会让 RES 维持在高位。jemalloc、tcmalloc 也有类似行为。这种涨上去下不来看起来像泄漏其实是分配器的正常策略用malloc_trim或者调整MALLOC_ARENA_MAX可以缓解。第三种是写时复制导致的页分裂。fork 出来的子进程在没写之前和父进程共享页算 SHR一旦写了共享页会分裂成两份独立的页子进程的 RES 就会上涨。这种增长是预期内的不是泄漏。真正的内存泄漏通常表现为RES 持续单调上涨、SHR 基本不变、进程的堆区看 smaps 里的 [heap] 段持续扩大、且这种增长和业务请求量没有明显相关性。只有这几个条件同时满足才值得怀疑泄漏。单看 RES 一个数字就下结论误判率非常高。2.3 共享库让 VIRT 虚高但不代表浪费前面提过共享库会映射进每个进程的 VIRT这里展开说一下它为什么容易让人误判。假设一台机器上跑了 50 个进程每个进程都链接了同一个 20M 的动态库。每个进程的 VIRT 里都会算上这 20M50 个进程加起来就是 1G 的虚拟占用。但物理内存里这个库的代码页只有一份 20M由 50 个进程共享。如果你只看 VIRT 加总会以为内存被吃掉了 1G实际上只用了 20M。这个特性在容器环境里尤其容易造成困惑。容器里跑的应用往往链接了大量基础库VIRT 看起来很大但实际物理占用很小。有些监控面板直接把 VIRT 当成内存使用展示导致运维人员频繁误报。正确的做法是监控 RES 和系统级可用内存VIRT 只作为辅助参考用来判断地址空间是否异常膨胀比如是否有异常的 mmap 或线程数暴增。想验证某个进程的 VIRT 里到底有多少是共享库映射可以看/proc/[pid]/maps把里面标记为r-xp且路径是.so的段大小加起来就能大致估算共享库贡献的虚拟占用。这个操作在怀疑 VIRT 异常时很有用能快速排除只是库多这种良性原因。2.4 用一张对照表把误判场景钉死为了让你在实战中快速定位我把常见的现象和对应的正确解读整理成表。这张表建议收藏排查时对着看能省不少时间。观察到的现象常见误判更可能的真实原因下一步查什么VIRT 几十 G进程占了几十 G 内存地址空间预留、共享库映射、线程栈/proc/[pid]/maps、线程数RES 持续上涨内存泄漏页缓存、分配器缓存、写时复制smaps 里 RssFile 与 [heap] 占比RES 加总超物理内存内存统计出错共享页被重复计算改用 free 看系统整体SHR 很大进程独占内存多共享库、共享内存段、mmap 文件smaps 里 Shared 相关段RES 突然下降进程释放了内存页被回收或换出看 swap 使用、系统内存压力3. 动手验证用 smaps 把每个数字拆到段级别3.1 为什么 smaps 比 top 更值得信任top 给的是汇总数字而/proc/[pid]/smaps给的是按内存段VMAvirtual memory area拆开的明细。每一段都会列出 Size、Rss、Shared_Clean、Shared_Dirty、Private_Clean、Private_Dirty 等字段。当你想知道 RES 到底花在哪、SHR 具体是哪些段贡献的smaps 是绕不开的工具。smaps 的统计比 statm 更细也更接近真相。比如你想确认某个进程的 RES 上涨是不是页缓存导致的直接看 smaps 里各个文件映射段的 Rss 之和再对比 [heap] 段的 Rss就能判断增长来自哪里。这种段级别的定位能力是 top 这种汇总视图给不了的。不过 smaps 有个代价读取它需要遍历进程的所有 VMA进程 VMA 很多时比如几万个 mmap 段读一次可能耗时几十毫秒甚至更久而且会短暂持有 mm 的锁。所以在生产环境高频采集 smaps 要谨慎容易对目标进程造成可观测的抖动。我的做法是只在排查时手动读或者用低频采样比如 30 秒一次并做好限流。3.2 一段可直接复用的 smaps 汇总脚本手动翻 smaps 太累下面这段 shell 能把关键汇总一次性算出来。它按段类型归类输出总 Size、总 Rss、共享部分和私有部分跑完你对这个进程的内存构成就一目了然了。#!/bin/bash # 用法: ./smaps_summary.sh pid pid$1 smaps/proc/$pid/smaps if [ ! -r $smaps ]; then echo 无法读取 $smaps检查 pid 或权限 exit 1 fi awk /^[0-9a-f]-[0-9a-f]/ { name$6; if (name) name[anon] } /^Size:/ { size[name]$2 } /^Rss:/ { rss[name]$2 } /^Shared_Clean:/ { sc[name]$2 } /^Shared_Dirty:/ { sd[name]$2 } /^Private_Clean:/ { pc[name]$2 } /^Private_Dirty:/ { pd[name]$2 } END { printf %-40s %10s %10s %10s %10s\n, 段名称, Size(KB), Rss(KB), Shared(KB), Private(KB) for (n in rss) { printf %-40s %10d %10d %10d %10d\n, n, size[n], rss[n], sc[n]sd[n], pc[n]pd[n] } } $smaps | sort -k3 -nr | head -30这段脚本的核心逻辑是按段名聚合然后按 Rss 降序取前 30 个。实际用的时候你会发现排在前面的往往是 [heap]、[stack]、几个大的 .so 文件、以及匿名 mmap 段。如果 [heap] 的 Rss 持续增长那才是真正需要警惕的信号如果排前面的是某个数据文件映射那基本就是页缓存。3.3 用 pmap 快速看地址空间分布如果只是想快速看 VIRT 的构成不想写脚本pmap是个更轻量的选择。pmap -x pid会列出每个映射段的地址、大小、RSS、脏页等最后给一个汇总。它比 smaps 输出更紧凑适合快速扫一眼。pmap -x 12345 | tail -5输出末尾的 total 行会给出总的 Kbytes对应 VIRT、RSS、Dirty。你可以拿这个 RSS 和 top 里的 RES 对照正常情况下应该很接近。如果差得比较多可能是采样时机不同或者 top 显示的是某个瞬间而 pmap 是另一个瞬间多跑几次取稳定值即可。pmap 的一个实用技巧是配合sort找大段。比如pmap -x pid | sort -k3 -nr | head -20能快速定位占用最大的映射段。这个操作在怀疑某个 mmap 异常膨胀时特别高效比翻 smaps 快得多。3.4 观察 RES 与 SHR 随时间的联动变化单次采样只能看到快照想看趋势得连续采。下面这个循环每 2 秒采一次把 VIRT、RES、SHR 打出来跑几分钟就能看出这个进程的内存行为模式。pid12345 while true; do awk -v p$pid { printf %s VIRT%dM RES%dM SHR%dM\n, strftime(%H:%M:%S), $1*4/1024, $2*4/1024, $3*4/1024 } /proc/$pid/statm sleep 2 done这里statm的字段单位是页乘以 4假设页大小 4K再除以 1024 换算成 M。第一个字段是 VIRT第二个是 RES第三个是 SHR。跑起来之后你可以一边压测一边观察如果 RES 涨而 SHR 不动多半是堆分配如果 RES 和 SHR 同步涨多半是文件映射或共享内存如果 RES 涨到某个点后稳定、SHR 也稳定那可能是分配器缓存到位了。提示页大小不一定是 4KARM 机器上常见 64K 页。用getconf PAGESIZE确认后再换算否则数字会差一个数量级。4. 真实排查场景从数字异常到定位根因4.1 场景一RES 缓慢爬升但系统内存充足先说一个我遇到过的典型情况。某服务上线后监控显示单个进程的 RES 从 500M 缓慢爬到 2G 多持续了好几天但系统整体可用内存一直很充裕也没有触发任何 OOM。值班的同学第一反应是内存泄漏准备重启。我让他先别急按前面的方法连续采了半小时的 statm同时看 smaps 的段分布。结果发现 RES 的增长几乎全部来自几个大的数据文件映射段[heap] 段的 Rss 基本没动。这就说明进程并没有在堆上泄漏只是随着业务运行读了越来越多的文件页缓存被算进了它的 RES。系统内存充足时这些页缓存不会被回收所以看起来只涨不降。进一步验证手动echo 1 /proc/sys/vm/drop_caches注意这会影响整机性能只在测试环境做再看 RES果然掉回了 600M 左右。这就坐实了是页缓存。结论是这个进程不需要重启监控上应该把 RssFile 和 RssAnon 分开看而不是盯着总 RES 告警。这个案例的教训是RES 上涨要先分清是匿名页RssAnon真正可能泄漏的部分还是文件页RssFile可回收的页缓存。smaps 里每个段的 Shared/Private 和文件路径能帮你快速区分。只看总 RES 就下结论很容易误杀一个其实很健康的进程。4.2 场景二VIRT 异常巨大导致误判另一个常见场景是 VIRT 大得离谱。有个同学跑来看说某个进程 VIRT 显示 128G机器才 64G 内存是不是要炸了。我让他先看线程数和 maps。一查这个进程开了 2000 多个线程每个线程栈预留 8M光线程栈就是 16G 的虚拟占用。再加上 JVM 或者运行时预留的大块地址空间、以及大量共享库映射VIRT 到 128G 完全说得通。实际 RES 只有 3G 左右系统运行平稳。这里的关键认知是VIRT 反映的是地址空间的账面规模不是物理内存压力。线程数多、预留大、库多都会推高 VIRT但只要 RES 和系统可用内存正常就没什么可担心的。真正需要警惕的 VIRT 异常是那种突然暴涨且伴随 RES 暴涨的情况那可能是有代码在疯狂 mmap 或者线程数失控。排查 VIRT 构成时/proc/[pid]/maps配合统计最直接。把 maps 里各段大小按类型汇总能看出是线程栈、共享库、匿名映射还是文件映射占了大头。这个操作我在前面 smaps 脚本里已经覆盖了直接复用即可。4.3 场景三SHR 占比过高背后的共享内存问题SHR 高本身不一定是坏事共享库多、用了共享内存做进程间通信都会让 SHR 偏高。但如果 SHR 高得离谱比如占了 RES 的 80% 以上就值得查一查是不是有共享内存段没释放。我遇到过一次某服务用 System V 共享内存做缓存进程重启后旧的共享内存段没被清理新进程又 attach 上去导致 SHR 一直很高而且ipcs -m里能看到一堆 nattch 为 0 的僵尸段。这些段占着物理内存不放虽然不算某个进程独占但整体上浪费了资源。排查方法是ipcs -m看共享内存段列表重点关注 nattchattach 进程数为 0 但段还存在的以及 size 异常大的。清理用ipcrm -m shmid但一定要确认没有进程还在用否则会出问题。POSIX 共享内存则在/dev/shm下用ls -lh /dev/shm能看到清理时同样要小心。这个场景提醒我们SHR 不只是一个观测指标它背后可能藏着共享内存的生命周期管理问题。定期检查共享内存段的使用情况比等到 SHR 告警了再查要主动得多。4.4 场景四容器里 top 数字和宿主机对不上容器环境是这类困惑的重灾区。容器里跑 top看到的 RES 是容器内进程的视角而宿主机上看到的又是另一套数字两边经常对不上。原因主要有两个一是容器内可能看不到完整的宿主机进程树二是 cgroup 的内存统计口径和进程级 RES 不是一回事。cgroup 的 memory.usage_in_bytescgroup v1或 memory.currentcgroup v2统计的是整个 cgroup 内所有进程加页缓存的总和它和单个进程的 RES 之和不是简单相等关系因为共享页在 cgroup 层面只算一次而进程级 RES 会重复计。所以容器里所有进程 RES 加起来大于 cgroup 的 memory.current 是正常的。排查容器内存问题时正确的姿势是先看 cgroup 的 memory.current 和 memory.stat里面有 anon、file、shmem 等细分再看容器内进程的 RES 分布。如果 memory.stat 里 file 占比很高说明主要是页缓存可以调memory.limit_in_bytes或者让内核自然回收如果 anon 持续上涨那才是真正的内存压力。把进程级 RES 和 cgroup 级统计混着看是容器内存排查最常见的错误。5. 把这些指标用进日常监控与容量判断5.1 监控该采哪些字段别只盯 RES如果让我给一套最小可用的进程内存监控字段我会选这几个RssAnon、RssFile、RssShmem、VmSize、以及线程数。RssAnon 是匿名页最接近真正可能泄漏的部分RssFile 是文件页缓存可回收RssShmem 是共享内存VmSize 对应 VIRT用来发现地址空间异常线程数用来解释 VIRT 的合理膨胀。这些字段都能从/proc/[pid]/status里直接读到不需要解析 smaps采集开销很小。status 里的 VmRSS 对应 RESRssAnon/RssFile/RssShmem 是它的细分VmSize 对应 VIRT。把这些做成时序指标比只采一个 RES 有价值得多。告警策略上我建议对 RssAnon 做趋势告警比如 1 小时内涨幅超过阈值而不是对 RES 做绝对值告警。因为 RES 受页缓存影响大绝对值告警误报率高。对 VIRT 则基本不需要告警除非你明确知道某个进程的 VIRT 有合理上限超过就说明有异常 mmap 或线程泄漏。5.2 容量规划时怎么估算真实内存需求做容量规划时最忌讳拿 VIRT 或者 RES 加总来估算。正确的做法是分层看先看系统级 MemAvailable 的趋势再看各进程 RssAnon 之和最后考虑页缓存的可回收空间。一个粗略但实用的估算公式是真实内存需求 ≈ 各进程 RssAnon 之和 各进程 RssShmem 去重后的量 必要的页缓存余量。RssShmem 去重比较麻烦实际中可以用 cgroup 的 memory.stat 里的 shmem 字段代替。页缓存余量一般留物理内存的 10% 到 20% 就够因为页缓存本身是可回收的不需要按峰值预留。还有一个经验值如果系统 MemAvailable 长期低于总内存的 10%且 RssAnon 之和持续上涨那才是真正需要扩容或者排查泄漏的信号。如果 MemAvailable 低但主要是页缓存占着系统会在压力下自动回收不必急着加内存。这个判断逻辑比看单个进程的 RES 靠谱得多。5.3 几个容易踩的采样陷阱最后说几个采样层面的坑这些是文档里不会写、但实际排查中经常遇到的。第一个坑是采样频率过高。前面提过 smaps 读取有锁开销高频读会让目标进程抖动。top 本身也有开销top -d 0.1这种高频刷新在进程数多的机器上会明显增加负载。排查时用 1 到 2 秒的间隔就够了没必要追求毫秒级。第二个坑是页大小假设错误。很多脚本默认页是 4K在 ARM 或者某些配置的机器上是 64K换算出来的数字会差 16 倍。写脚本前先getconf PAGESIZE确认或者直接用 status 里已经换算好的 KB 值避免自己算错。第三个坑是容器内 /proc 的可见性。容器里读/proc/[pid]/statm时如果 pid 是宿主机命名空间的可能读不到或者读到的是错误数据。排查容器内存优先用 cgroup 接口进程级指标只作为辅助。第四个坑是把 top 的 %MEM 当权威。%MEM 是 RES 除以物理内存总量算出来的它同样继承了 RES 的所有局限含共享页、含页缓存而且分母是物理内存总量而不是可用内存参考价值有限。判断内存压力还是以 MemAvailable 和 RssAnon 为准。把这些采样细节处理好你看到的数字才可信后面的分析才有意义。VIRT、RES、SHR 这三个参数本身不复杂复杂的是它们背后的内存模型和统计口径。把模型理顺了再配合 smaps、pmap、cgroup 这些工具做交叉验证绝大多数内存困惑都能在几分钟内定位清楚。我自己现在排查内存问题基本就是先看 status 的几个细分字段定方向再用 smaps 定位到具体段最后结合系统级指标下结论这套流程跑下来很少走弯路。