1. 为什么这几个参数值得单独拎出来讲
刚接触 Linux 那会儿,我对top命令的态度基本是“能看懂个大概就行”。CPU 占用高了就盯着%CPU看,内存紧张了就扫一眼free,至于进程列表里那几列VIRT、RES、SHR,说实话,很长一段时间我都是直接跳过的。直到有一次线上服务被系统 OOM 干掉,我打开top一看,某个进程的VIRT显示 20 多个 G,当时第一反应是“这进程怎么吃了这么多内存”,差点就去改代码了。后来冷静下来逐列核对,才发现VIRT大得离谱但RES其实只有几百兆,真正的问题根本不在这个进程身上。
这件事之后我才认真把这几列的含义捋了一遍。top命令里的VIRT、RES、SHR是内存维度的三个核心指标,分别对应虚拟内存集、常驻内存集和共享内存。它们看起来都是“内存”,但统计口径完全不同,混着看非常容易得出错误结论。这篇内容就是想把这三个参数掰开揉碎讲清楚:它们各自统计的是什么、数值为什么会差这么多、什么场景下该看哪一个、以及实际排查问题时怎么组合使用。适合已经会用top但对其内存列一知半解的朋友,也适合刚上手 Linux 运维、需要快速建立内存观测直觉的同学。前 100 字里我反复提到这几个关键词,是因为它们确实是理解进程内存行为的地基,绕不开。
我个人的经验是,很多人对top的误解不是出在“不会用”,而是出在“看错列”。%CPU和load average相对直观,但内存这几列一旦理解偏差,排查方向就会整个跑偏。所以与其泛泛地讲top怎么用,不如把这三列单独拿出来做一次彻底拆解,把背后的统计逻辑、常见误区和实操判断方法都过一遍。
2. 三个参数的本质:先建立正确的内存模型
2.1 VIRT 是“申请了多少”,不是“用了多少”
VIRT全称是 Virtual Memory Size,中文一般叫虚拟内存集。它统计的是进程地址空间的大小,也就是这个进程向内核“申请”或“映射”了多少虚拟内存。注意关键词是“申请”和“映射”,而不是“实际占用”。
这里需要先建立一个基本认知:现代操作系统给每个进程提供的都是虚拟地址空间,进程看到的地址并不是物理内存的真实地址,中间隔着一层页表映射。进程启动时,内核会给它划出一大块虚拟地址范围,代码段、数据段、堆、栈、共享库映射、内存映射文件等等,全都算在这块地址空间里。VIRT统计的就是这块地址空间的总大小。
所以VIRT偏大是极其正常的现象。一个普通的 Java 进程,VIRT动辄几个 G 甚至十几个 G,但实际物理内存可能只占几百兆。原因在于 JVM 会预留大块虚拟地址空间用于堆管理、元空间、线程栈等,这些预留并不等于真实消耗。同理,很多服务会mmap大文件,映射进来的地址空间也算VIRT,但物理页只有在真正访问时才按需加载。
提示:看到
VIRT很大先别慌,它更像“预算”而不是“支出”。判断内存压力要看RES,VIRT只在你怀疑地址空间泄漏或者映射异常时才有参考价值。
我踩过的坑就是开头说的那次,把VIRT当成了实际内存占用。后来养成习惯,看进程内存第一眼先看RES,VIRT只在特定场景下才深究。这个顺序调整过来之后,误判率直线下降。
2.2 RES 才是真正压在物理内存上的部分
RES全称 Resident Set Size,常驻内存集。它统计的是进程当前实际驻留在物理内存中的页大小,不包括被换出到交换分区的部分。换句话说,RES才是这个进程此刻真正占着物理内存的量,也是判断内存压力时最该盯着的指标。
RES的构成比较复杂,它包含了进程私有的匿名页(比如堆上分配且已实际写入的数据)、私有文件映射页、以及共享内存中属于该进程的那部分。注意最后这一点很关键:RES里是包含共享页的。这就引出了SHR的存在意义。
举个生活化的例子。假设一个办公室里几个人合看同一份文件,RES相当于“这个人桌上摆着的所有纸张”,其中包含了他和别人共用的那份文件;而SHR就是“其中属于大家共用的那部分纸张”。所以如果你把多个进程的RES直接相加,共享部分会被重复计算,得到的数字会虚高。这也是为什么用top逐个加RES去估算总内存占用,往往会比free显示的实际使用量偏大。
RES会随着进程实际访问内存而增长,也会因为内核回收、页面换出而下降。它是个动态值,不是分配后就固定不变的。实际观察时会发现,一个刚启动的服务RES可能只有几十兆,跑一段时间、缓存热起来之后涨到几百兆,这是正常的“预热”过程,不代表内存泄漏。
2.3 SHR 是 RES 里被共享的那一块
SHR全称 Shared Memory,共享内存。它表示RES中可能与其他进程共享的那部分内存。注意“可能”两个字,这是理解SHR的关键,也是很多人困惑的地方。
SHR主要来自几类来源:共享库(比如多个进程都链接了同一个.so文件)、共享内存段(shm)、以及内存映射的共享文件。这些页在物理内存中只有一份,被多个进程的页表同时指向,所以每个进程的RES里都会把这部分算进去,但物理上并没有重复占用。
这里有个容易误解的点:SHR统计的是“可共享”的页,而不是“正在被多个进程实际共享”的页。一个进程独占映射了一个共享库文件,物理上可能只有它自己在用,但这一页依然会被计入SHR。所以SHR偏大不代表一定省了内存,它只是告诉你“这部分有共享的潜力”。
实际排查时,SHR的典型用途是判断内存是否被重复计算。比如你发现几个同类进程RES加起来特别大,但系统整体内存并没有那么紧张,多半就是共享库和共享映射在起作用,SHR能帮你把这部分“虚账”识别出来。
2.4 三者的关系用一张表说清
| 参数 | 全称 | 统计口径 | 是否含共享 | 典型用途 |
|---|---|---|---|---|
| VIRT | Virtual Memory Size | 进程虚拟地址空间总大小 | 含映射部分 | 排查地址空间异常、映射泄漏 |
| RES | Resident Set Size | 实际驻留物理内存的页 | 含共享页 | 判断真实内存压力 |
| SHR | Shared Memory | RES 中可共享的部分 | 本身即共享口径 | 识别重复计算、评估共享收益 |
从包含关系上看,可以粗略理解为VIRT是最大的集合,RES是其中真正落到物理内存的子集,SHR又是RES里可共享的那部分子集。三者不是简单的加减关系,但层级上是VIRT ⊇ RES ⊇ SHR的大致结构。理解这个层级,后面看数值就不会乱。
3. 实操:在 top 里把这三列调出来并读懂
3.1 默认视图里它们藏在哪
直接敲top进入交互界面,默认的进程列表里其实已经包含了这几列,只是列比较多,屏幕窄的时候会被截断。默认列顺序大致是PID、USER、PR、NI、VIRT、RES、SHR、S、%CPU、%MEM、TIME+、COMMAND。也就是说VIRT、RES、SHR是挨着的三列,位置在优先级相关列之后、状态列之前。
如果你的终端比较窄,可能会看到RES后面的列被省略。这时候有两个办法:一是把终端窗口拉宽,二是用top的列管理功能主动裁剪不需要的列。我一般会把PR、NI、S这些平时不太看的列去掉,给内存列腾出空间,看起来更清爽。
进入top后按f键可以进入字段管理界面,用方向键移动、空格键切换显示状态,选好后按q或回车返回。这个界面里能看到所有可用字段的完整名称,也是确认列含义最权威的地方,比查各种二手资料靠谱。
3.2 用交互键快速切换排序和单位
看内存最常用的操作是按M键,让进程列表按RES降序排列,内存大户立刻排到最前面。这是排查内存问题时我按得最多的一个键。如果想按VIRT排序,可以在字段管理里把排序依据切过去,或者用top -o VIRT启动时直接指定。
单位显示也值得调一下。默认top对内存列用的是 KiB 为单位,数值大了之后位数很多,不好快速判断量级。按E键可以切换顶部汇总区的单位(KiB、MiB、GiB 循环),按e键切换进程列表内存列的单位。我习惯把进程列表切成 MiB 显示,几百兆、几个 G 一眼就能分辨,不用数零。
还有一个实用技巧是按Shift + E(大写 E)切换汇总内存的显示单位,配合小写e分别控制汇总区和进程区的单位,两者独立,可以按自己习惯搭配。这些按键在top里按h都能查到帮助,建议花两分钟过一遍,比记博客笔记可靠。
3.3 用批处理模式抓取可对比的快照
交互界面适合实时观察,但要做前后对比或者记录排查过程,批处理模式更好用。top -b -n 1表示批处理模式只输出一次,配合-o RES可以按常驻内存排序输出。这样抓下来的文本可以直接存文件、做 diff,或者贴到排查记录里。
# 按 RES 降序抓一次快照,取前 20 行 top -b -n 1 -o RES | head -n 20 # 连续抓 5 次,每次间隔 2 秒,用于观察 RES 增长趋势 top -b -n 5 -d 2 -o RES > mem_snapshot.txt批处理模式下没有交互界面的高亮和动态刷新,但胜在可脚本化。我排查内存缓慢增长类问题时,经常用连续快照的方式,把同一个进程的RES随时间的变化拉出来看,是稳定在一个水位还是持续爬升,一眼就能判断是不是真泄漏。这比盯着交互界面凭感觉判断靠谱得多。
注意:批处理模式第一次采样可能不准,因为
top需要两次采样才能算出准确的 CPU 占用率。看内存列不受这个影响,但如果同时要看%CPU,建议-n至少给 2,取第二次之后的数据。
3.4 结合 %MEM 一起看才完整
RES是绝对值,%MEM是它占物理内存总量的百分比。单看RES有时候不好判断严重程度,比如一台 128G 的机器上某进程占 2G,和一台 8G 的机器上占 2G,性质完全不同。%MEM能帮你快速定位“这个进程在当前机器上算不算大户”。
我的习惯是先用M按RES排序,扫一眼排在前面的进程的%MEM,如果头部进程%MEM加起来已经占了很大比例,那内存压力基本就实锤了。然后再逐个看这些进程的RES和SHR比例,判断是私有内存吃得多还是共享部分占大头。这个组合判断的流程,比单看任何一列都更接近真相。
4. 数值异常时怎么判断和排查
4.1 VIRT 巨大但 RES 很小,正常吗
非常正常,而且是最常见的情况。前面说过VIRT是地址空间预算,很多运行时环境会预留大块虚拟地址。比如某些语言的运行时在启动时就预留几个 G 的堆地址空间,实际用多少再按需提交;再比如进程mmap了大文件但只访问了其中一小部分,映射的地址空间全算VIRT,物理页只加载了访问到的部分。
判断标准很简单:只要RES在合理范围、系统整体内存没有压力、进程运行正常,VIRT大就不用管。真正需要警惕的是VIRT持续异常增长且伴随RES同步增长,那可能是地址空间泄漏或者映射没有释放。单纯VIRT大而RES稳,基本可以忽略。
我见过有人一看到VIRT几十 G 就紧张,甚至去调各种参数限制虚拟内存,结果把正常服务搞出问题。这个方向从一开始就错了,VIRT不是用来判断内存压力的指标,别在它身上浪费精力。
4.2 RES 持续上涨,是泄漏还是缓存
RES上涨有两种性质完全不同的情况。一种是进程真的在持续分配并持有内存,比如缓存越攒越多、对象没有释放,这种是潜在泄漏;另一种是进程在预热,把常用数据加载进内存,涨到某个水位后会稳定下来,这是健康的缓存行为。
区分方法就是看趋势形态。用前面说的连续快照方式,观察RES是“涨到某个值后走平”还是“无上限持续爬升”。前者大概率是正常缓存,后者才需要怀疑泄漏。另外可以结合业务表现看,如果RES涨的同时请求延迟、GC 频率、错误率都正常,那多半没事;如果伴随性能劣化,就要深入查了。
还有一个辅助判断是看RES和VIRT的比例变化。如果VIRT基本不动而RES一路涨,说明是在已预留的地址空间里不断提交物理页,这种更偏向真实使用增长;如果两者同步暴涨,可能是新的映射在不断创建,方向就不一样了。
4.3 多个进程 RES 相加远超实际用量
这是共享内存导致的重复计算,前面提过。多个进程链接同一个共享库、映射同一个共享文件,这部分物理页只有一份,但每个进程的RES都会把它算进去。进程数越多、共享库越大,重复计算的量就越可观。
验证方法是看这些进程的SHR。如果每个进程的SHR都不小,且它们共享的是同一批库或文件,那RES相加虚高就解释得通了。想估算真实占用,可以用RES - SHR得到大致的私有内存量,再把各进程私有部分相加,加上共享部分的单份占用,会更接近实际。当然这只是估算,精确值还是以系统级内存统计为准。
实际排查时我一般不会去手工做这个加减,而是用SHR的存在来解释“为什么相加对不上”,避免被虚高的数字带偏判断方向。知道有这么回事,看到异常数字时能想到共享因素,就够用了。
4.4 常见现象速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| VIRT 极大、RES 正常 | 地址空间预留、大文件映射 | 一般无需处理,关注 RES 即可 |
| VIRT 与 RES 同步持续增长 | 映射泄漏、地址空间未释放 | 检查 mmap 调用、映射生命周期 |
| RES 涨到水位后走平 | 正常缓存预热 | 结合业务指标确认,通常无需干预 |
| RES 无上限持续爬升 | 潜在内存泄漏 | 连续快照看趋势,结合 GC/分配日志 |
| 多进程 RES 相加远超总量 | 共享库、共享映射重复计算 | 对比 SHR,识别共享部分 |
| SHR 异常大 | 大量共享映射或共享内存段 | 检查共享库依赖、shm 使用情况 |
这张表是我自己排查时总结的快速对照,实际遇到现象先对号入座,能省不少瞎猜的时间。当然具体问题还得结合现场,表格只是缩小排查范围的起点。
5. 几个容易踩的坑和实操心得
5.1 别用 top 的 RES 去算系统总内存
这是新手最容易犯的错。把进程列表里所有RES加起来,得到的数字往往比free显示的实际使用量大一截,原因就是共享部分被重复计算了。系统级的内存使用情况,free或者/proc/meminfo才是准确来源,top的进程级RES只适合看单个进程的相对大小,不适合做全局汇总。
我早期做容量评估时就吃过这个亏,按RES相加估出来内存快满了,实际free一看还有大量可用,虚惊一场。后来定了个规矩:进程级看top,系统级看free,两者不混用,判断就稳了。
5.2 SHR 大不等于省了内存
SHR统计的是“可共享”的页,不是“实际被多方共享”的页。一个进程独占映射的共享库,物理上只有它在用,但依然计入SHR。所以看到SHR大就认为“这部分内存被省下来了”,是不准确的。它只能说明这部分有共享的潜力,实际省没省要看是否真有其他进程映射了同一份。
这个认知偏差在评估容器内存时特别容易出问题。容器里进程的SHR可能不小,但如果宿主机上没有其他进程共享这些页,那这部分内存并没有被摊薄。判断共享收益,得看实际共享关系,不能只看单进程的SHR数值。
5.3 单位不统一是误判重灾区
top默认用 KiB 显示内存,但很多文档、监控系统用 MiB 或 GiB,换算时差着 1024 倍。我见过有人把RES的 102400(KiB,约 100MiB)当成 100GiB 来理解,直接吓一跳。养成习惯,看数值先确认单位,top里按e和E把单位调成自己顺眼的,能避免大量低级误判。
监控系统对接时更要注意,不同工具对RES的采集口径和单位可能不一致,做对比分析前先对齐单位,否则图表画出来趋势都是错的。这个坑不深但很常见,提一句提醒大家。
5.4 采样时机影响判断
RES是动态值,采样时机不同结果可能差很多。进程刚启动、正在处理大批量请求、刚触发过 GC,这些时刻的RES都可能出现尖峰或低谷。单次采样看到一个偏高的值就下结论,容易误判。
我的做法是至少连续观察一段时间,看趋势而不是看单点。批处理模式连续抓几次,或者交互界面盯个一两分钟,确认数值是稳定、波动还是单向变化,再下判断。内存问题大多是趋势性问题,单点快照的信息量有限。
5.5 结合进程的实际行为去解读
脱离进程行为单纯看数字,很容易得出片面结论。一个数据库进程RES大,可能是正常的缓冲池;一个 Web 服务RES大,可能是连接数多或者缓存策略激进;一个批处理任务RES阶段性冲高,可能只是处理大文件时的临时占用。同样的数值,在不同进程身上含义完全不同。
所以看这三列的时候,我心里会同时挂着几个问题:这个进程是干什么的、它的内存使用模式应该是什么样、当前数值和预期是否吻合。带着这些背景去看数字,判断会准很多。纯粹的数字对比,参考价值有限。
6. 把这三列用成一套组合判断方法
单独看VIRT、RES、SHR任何一列,信息都是不完整的,真正有用的是把它们组合起来形成判断链条。我自己的习惯流程是这样的:先用M按RES排序找到内存大户,看它的%MEM判断严重程度;然后看这个进程的SHR占比,如果SHR很大,说明有相当部分是共享的,实际私有占用没那么夸张;再看VIRT和RES的比例,如果VIRT远大于RES,说明地址空间预留多但实际提交少,属于正常现象;最后结合连续采样的趋势,判断是稳定、波动还是持续增长。
这个链条走下来,大部分内存相关的疑问都能有个初步结论。比如开头那次 OOM 排查,如果当时按这个流程走,第一步看RES就会发现那个VIRT20 多 G 的进程实际只占几百兆,根本不会是元凶,能省下不少走弯路的时间。
再补充一个组合视角:RES - SHR可以粗略看作进程的私有常驻内存,这个值在评估“这个进程独占了多少物理内存”时比较有参考意义。多个同类进程对比时,看私有内存比看RES更能反映各自的真实差异,因为共享部分被剔除了。当然这依然是估算,但方向是对的。
实际工作中,这三列更多是帮你快速定位和缩小范围,真要精确分析还得结合/proc/<pid>/status、/proc/<pid>/smaps这些更细粒度的数据源。top的优势是快、直观、一眼能看到全局,适合做第一轮筛查。把这三列的含义和组合判断方法吃透,第一轮筛查的准确率会高很多,后续深入排查也能少走弯路。
我个人在实际操作中的体会是,工具的参数含义只是表层,真正值钱的是知道“什么场景下该信哪个数、什么场景下这个数会骗你”。VIRT、RES、SHR这三个参数,把它们的统计口径和相互关系搞清楚之后,看top的心态会完全不一样,从“看个热闹”变成“看得明白”。最后再分享一个小习惯:每次排查完内存问题,把当时的top快照和结论简单记一笔,攒一段时间回头看,会发现自己对这几个参数的直觉越来越准,比临时翻文档管用得多。