首页 / 资讯中心 / 文章详情

jstat实战:用JDK自带工具监控JVM内存与GC

jstat实战:用JDK自带工具监控JVM内存与GC ★ FEATURED ARTICLE
在Java服务卡顿、内存飙高、接口超时的时候绝大多数人第一反应是抓一把jstack看线程或者拉一份heap dump慢慢分析。但很多时候问题的根源根本不在某个线程卡在哪行代码而是整个堆的“新陈代谢”出了问题——对象疯狂创建、回收跟不上、老年代不断膨胀但迟迟不触发Full GC。这种时候jstack帮不了你heap dump又太重真正该掏出的是JDK自带的那个低调到容易被忽略的轻量级监控命令jstat。jstat全称是JVM Statistics Monitoring Tool是JDK命令行工具家族里专门盯JVM运行期统计信息的利器。它不抓线程栈、不导堆快照只做一件事持续输出JVM各个区域的内存占用、GC次数、GC耗时、类加载数量、编译统计等量化指标。这听起来朴素但恰恰是性能排查里最缺的第一手数据——你连GC频率和内存增长趋势都不知道谈优化方向就是闭着眼猜。这篇就专门聊聊jstat。我会从最基本的命令参数讲起把每个常用参数的输出字段都掰开揉碎然后结合真实排查场景演示怎么用jstat定位“老年代持续增长但Full GC迟迟不来”这类经典问题最后分享一些我实际踩过的坑和排查技巧。不管你是刚接触JVM调优的新手还是被线上问题折磨过的老手这篇都值得花几分钟看完。1. 内容整体设计与思路拆解1.1 jstat到底解决什么问题先说个真实的场景。某天线上一个订单处理服务突然出现大面积超时CPU不高内存也没到OOM但接口RT响应时间就是降不下来。运维同事查了一圈说老年代占用率一直在90%徘徊但Full GC的日志看起来又不频繁。这时候你手头有什么工具jstack能告诉你线程在等什么锁但没法告诉你为什么锁迟迟释放不了。jmap能dump堆内存但几个G的文件导出来分析一圈下来可能黄花菜都凉了。jstat的定位恰好补上这个盲区——它能在几秒钟内告诉你Eden区是不是已经塞满、Survivor区是不是在反复搬运对象、老年代是不是在被缓慢撑大、Young GC和Full GC分别多久来一次、每次耗时多少。所以我个人的习惯是遇到任何疑似JVM层面的性能问题第一件事不是上重工具而是先跑一轮jstat用几组数据把JVM的“健康画像”勾勒出来再决定后续用哪个工具深挖。这一步就像是医生先量体温、测血压而不是直接开核磁共振。1.2 为什么选jstat而不是其他监控工具市面上做JVM监控的富工具很多像VisualVM、JConsole、Arthas甚至各种APM全家桶都能展示GC曲线。那为什么命令行里的jstat还是不可或缺原因有三点。第一jstat是JDK自带的任何一台装了JDK的机器都能直接用不需要额外安装、不需要开agent、不需要改启动参数在排查生产环境问题时特别友好。很多公司对生产机器有严格的安全限制想上传一个Arthas的jar都费劲但jstat是原生命令几乎没有被限制的可能。第二jstat几乎零侵入、零性能损耗。它读取的是JVM内部的统计计数器不需要对应用做任何字节码增强也不会强制触发GC。在吞吐量敏感的服务上你甚至可以开着jstat持续监听不用担心它本身成为性能瓶颈。第三jstat的输出是纯文本的统计数据非常适合脚本化采集和趋势分析。你可以在bash里写个循环每隔几秒采一次jstat -gcutil的数据攒成一段时序数据再丢进Excel或任何图表工具里画趋势线。这种灵活度是图形化工具很难给的。1.3 和其他工具的分工配合jstat不是万能的它和jmap、jstack、jinfo更像是一个完整的排查链条。jstat看JVM运行状态的核心指标回答“现在到底怎么了”。jmap导出堆快照或查看堆内存的详细分布回答“内存里到底装了什么”。jstack抓线程快照回答“线程都在哪里卡住了”。jinfo查看和调整JVM启动参数回答“当前参数配置是什么”。用jstat发现问题用jmap和jstack定位细节用jinfo确认参数合理性这就是一个标准的JVM问题排查流程。后面我会结合案例演示这个流程怎么串起来。2. 核心细节解析与实操要点2.1 jstat命令格式和参数速览jstat的基本语法很简单jstat -选项 进程ID [采样间隔] [采样次数]中间那个进程ID是Java进程的PID可以用jps -l查出来。后面的采样间隔和次数可选项不写就是只打一次。举个例子你想每2秒采一次GC数据一共采5次命令就是jstat -gcutil 12345 2000 5这里12345换成你的Java进程PID。jstat的选项比较多但日常排查真正常用的就那几个我按使用频率排个序选项作用最常用字段-gcutil查看各内存区域的使用率百分比和GC时间S0、S1、E、O、M、YGC、FGC、FGCT-gc查看各内存区域的当前容量和已用量S0C、S1C、EC、OC、OU以及GC次数和时间-gccause在-gcutil基础上额外显示最近GC的原因LGCC、GCC-class查看类加载/卸载数量Loaded、Bytes、Unloaded-compiler查看JIT编译统计Compiled、Failed、Time-gccapacity查看各区域从初始值到最大值的容量变化NGCMN、NGCMX、OGCMN、OGCMX很多初学者一上来就盯着-gc那一大串字段懵了其实排查时最优解是先跑-gcutil管占比、再看需求用-gccause查原因性价比最高。2.2 输出字段逐个拆解jstat -gcutil的输出大概是这个样子S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 12.50 0.00 58.23 67.89 95.12 94.50 218 3.891 5 1.230 5.121这里的每个字段都有明确含义S0、S1两个Survivor区幸存者区当前的使用率百分比。Survivor区在复制算法里扮演“中转站”的角色Young GC时存活对象会被复制到其中一个区。正常情况两个区不会同时都很高因为同一时刻只有一个Survivor区在接收存活对象。EEden区的使用率百分比。Eden区是对象“出生”的地方绝大多数对象在这里被创建、死去。Eden区经常被打满其实是正常现象因为它打满就意味着Young GC要开始了。O老年代的使用率百分比。这是排查内存泄漏时最需要盯的字段。老年代持续增长且不回落往往是强引用对象越积越多的信号。M元空间Metaspace的使用率百分比。JDK8以后方法区被元空间取代存放类元数据。应用动态生成大量类比如反射、CGLIB代理时这个值会明显攀升。CCS压缩类空间Compressed Class Space的使用率百分比。YGC、YGCTYoung GC的次数和累计耗时秒。YGCT是累计值不是单次耗时。FGC、FGCTFull GC的次数和累计耗时秒。GCTGC的总累计耗时YGCT FGCT之和。看清楚这些字段就明白了-gcutil更像是一张“仪表盘”告诉你每个区域的健康状态和GC压力。而-gc选项则在此基础上多给出容量数据字段形如S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 7168.0 7168.0 896.0 0.0 57344.0 33432.5 131072.0 88977.3 51200.0 48765.4 6144.0 5812.3 218 3.891 5 1.230 5.121区别在于-gc给出的是具体的KB容量值和已用量-gcutil给出的是百分比。实际使用中我通常先看-gcutil快速评估健康状况确认某区域异常增长后再切到-gc看具体容量空间够不够。2.3 为什么采样间隔和次数很关键jstat单次采样的参考价值有限至少连续采样才能看出趋势。我建议排查问题时用1秒或2秒的间隔采5到10次观察每个区域的变化方向和GC频率。举个例子如果单看到老年代占用率是80%你很难判断是正常蓄水还是泄漏。但如果你每秒采一次连续采8秒发现老年代占用率从75%、77%、79%、81%一路涨上去Young GC又频繁得像打地鼠一样那基本可以断定对象的晋升速度异常必须引起重视。单次采样看状态连续采样看趋势这是jstat用法的核心心法。另外采样间隔不要太长否则趋势信息被稀释也不要太短否则大量输出反而干扰判断。生产环境上我一般用1000毫秒间隔采10次总共10秒钟就能对JVM状态有个基本判断。2.4 一个容易被忽略的细节进程PID别搞错在一台机器上跑着多个Java进程的时候jps -l的输出要仔细看别只图省事用jps不加参数。jps -l会显示完整的main类名或jar包路径能帮你准确区分是哪条业务线的进程。有一次我排查问题机器上有两个Java进程一个是业务服务一个是定时任务平台PID挨得特别近。我随手用jps看了个短PID就开跑jstat结果分析半天数据对不上号最后才发现盯错了进程。这种低级错误浪费的不是几分钟而是整个排查方向。3. 实操过程与核心环节实现3.1 用jstat给JVM拍一张“状态快照”先演示最基础的用法。假设我们通过jps -l查到目标Java进程的PID是25412先看一下类加载情况jstat -class 25412输出示例Loaded Bytes Unloaded Bytes Time 8235 15833.2 0 0.0 4.72这里看到加载了8235个类说明应用启动到现在类加载是收敛的没有在运行期疯狂加载新类。如果这个数字在持续飙升就要怀疑是不是有动态生成类的逻辑在作祟。再看编译统计jstat -compiler 25412输出示例Compiled Failed Invalid Time FailedType FailedMethod 4521 0 0 18.32 0JIT编译了4521个方法没有编译失败。这一步主要是确认JIT没有异常正常情况下后续不需要过多关注。然后是重点GC和内存区域的整体状态jstat -gcutil 25412输出示例S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 24.50 0.00 72.13 89.65 94.12 90.50 1532 12.891 12 8.230 21.121从这组数据能读出什么信息YGC已经发生了1532次累计Young GC耗时12.8秒Full GC只有12次但FGCT已经8.2秒平均每次Full GC耗时接近0.7秒——这个耗时明显偏高。老年代占用89.65%逼近临界值。整体判断这个JVM已经处于高压状态GC参数需要调整。3.2 连续采样看趋势定位老年代持续增长问题继续用上面的PID做个10次、每次间隔1秒的采样jstat -gcutil 25412 1000 10假设输出结果里O那一列的数据是这样的序列89.65、90.12、90.78、91.45、92.03、92.67、93.28、93.91、94.52、95.14。这个序列10秒内涨了5个多百分点说明老年代在以肉眼可见的速度膨胀。这时候再配合观察YGC的数值是不是也在快速增长就能拼出一个完整的故事Young GC非常频繁可能每秒都要触发好多次但每次Young GC后依然有大量对象因为Survivor区放不下而直接晋升到老年代老年代被持续撑大而Full GC又迟迟没触发触发时因为老年代接近满耗时又特别长。这种模式很典型的诱因是创建了大量生命周期偏长的对象、缓存没有上限、或者Survivor区配置过小导致对象过早晋升。有了jstat给出的数据方向下一步的选择就清晰了——要么用jmap看老年代里都是什么对象要么review代码里的缓存和集合使用方式。3.3 用jstat -gccause确认Full GC的触发原因-gcutil能告诉你“现在是什么状态”-gccause能告诉你“最近的GC是为什么发生的”jstat -gccause 25412输出示例LGCC GCC Allocation Failure System.gc()LGCCLast GC Cause显示最近一次GC的原因是Allocation Failure——也就是分配对象时内存不足触发的GCCCurrent GC Cause显示当前触发GC的原因是System.gc()——说明有代码显式调用了System.gc()。有意思的地方就在这里。当生产代码里有人调用了System.gc()配合某些JVM参数比如-XX:DisableExplicitGC没配上会导致Full GC被人为地频繁触发。-gccause在这种场景下就是一个“元凶雷达”一把抓住显式GC调用。我遇到过不止一次团队里有人为了“主动清理内存”在业务代码里写了System.gc()结果线上Full GC频繁、接口毛刺不断。最后就是用jstat -gccause锁定System.gc()这个原因的。3.4 结合场景一个典型的GC压力排查全流程说一个我实际处理过的模拟场景。某数据同步服务每隔一段时间会出现明显的卡顿每次持续一两秒人工观察时服务又能恢复。第一步先跑jstat -gcutil 25412 1000 10S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 80.12 0.00 95.34 42.55 88.10 85.20 5421 23.451 0 0.000 23.451 ...注意几点YGC已经5421次但FGC还是0。Young GC非常频繁YGCT累计时间在快速增长但老年代才用到42%左右。每次Young GC的耗时算下来平均不过几毫秒但累计时间在涨说明Young GC频率极高。第二步连续多采几轮观察YGC增长速率。假设每5秒YGC增加100次左右算下来每秒20次Young GC这明显不正常。正常服务每秒能有几次Young GC已经算忙了20次意味着Eden区被迅速塞满说明对象的分配速率极高。第三步用jstat -gc 25412确认Eden区容量的配置S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 512.0 512.0 408.2 0.0 4096.0 3904.8 8192.0 3444.7 20480.0 18040.2 2560.0 2180.3 5421 23.451 0 0.000 23.451Eden区只有4MBSurvivor区只有512KB。这个堆配置对于数据同步服务来说过小了。Eden区太小导致对象刚分配没多久就触发Young GCYoung GC后存活对象进入512KB的Survivor区空间不足又直接晋升老年代。到这里问题指向已经很明确不是代码bug而是JVM堆空间规划不合理。后续调整方向是增大堆内存、合理设置新生代比例或者改用G1并调整-XX:MaxGCPauseMillis目标。这就是jstat给出的数据和方向。3.5 做个可视化的小脚本方便持续观察命令行输出看久了眼睛容易花我习惯用一个小脚本把关键指标提取出来形成一行更紧凑的摘要jstat -gcutil 25412 1000 10 | awk NR1 {print $0} NR1 {printf E:%s%% O:%s%% YGC:%s FGC:%s GCT:%.3f\n, $3, $4, $7, $9, $11}这段awk只是把Eden占比、老年代占比、Young GC次数、Full GC次数和总GC耗时摘出来。真要长时间持续监测也可以结合date命令给每行加个时间戳jstat -gcutil 25412 1000 1000 | while read line; do echo $(date %H:%M:%S) $line; done这样积累一段时间的数据拉到Excel里画折线图老年代的Trend曲线一眼就能看出有没有泄漏苗头。我在做容量评估时经常这么干用一晚上的数据判断服务的GC水位是否稳定。4. 常见问题与排查技巧实录4.1 jstat查看远程进程或容器进程怎么办很多服务现在跑在容器里直接在宿主机上执行jps不一定能列出容器内的Java进程。这时要么进容器内部执行jstat要么在宿主机上用ps -ef | grep java找到Java进程的PID再执行jstat。注意容器PID namespace隔离的问题宿主机上看到的PID和容器内看到的可能不一致最稳妥的做法是直接docker exec进容器里执行jstat。如果遇到Unable to open socket file之类的报错通常是进程的/tmp目录下有权限问题或者Java进程不是当前用户启动的。最简单的排查方式是用ps -ef确认进程的启动用户然后用相同用户切换身份执行命令或者临时调整java.io.tmpdir参数但这个需要重启进程不是紧急场景建议做的。4.2 jstat显示数据不变或指标异常怎么办有时候jstat输出的数据看起来“僵住”了YGC和各项数值一直不变。这种时候先别慌着下结论很可能是这几秒内JVM确实没有触发任何GC不代表出了问题。如果应用本身有持续的内存分配但YGC停留在同一个数字很久这反而说明当前堆空间很充裕对象分配不需要频繁触发GC。反过来如果YGC数字跳得飞快但YGCT的增量却很小说明Young GC本身很快问题在于频率过高这是典型的“分配速率过高”或“堆太小”信号。如果YGC频率不高但YGCT单次耗时很长就要考虑是不是有大量对象在Survivor区反复复制或者当前GC算法在特殊场景下性能不佳。最诡异的情况是jstat显示FGC0但线上明显有停顿感。这种时候要怀疑是不是STWStop The World停顿并非来自GC而可能是JIT编译导致的-compiler选项可以辅助判断、偏向锁撤销导致的、或者线程转储时锁竞争导致的。jstat只是GC统计工具它说没GC不代表JVM真的没停顿过。4.3 jstat输出字段含义对照速查表为了省去每次查文档的麻烦我把最常用的字段整理成一张速查表贴在这里方便收藏字段含义使用建议S0C / S1CSurvivor 0/1区容量KB判断Survivor是否太小过小导致对象直接晋升老年代S0U / S1USurvivor 0/1区已用量KB结合S0/S1百分比看复制算法工作状态EC / EUEden区容量和已用量EU接近EC意味着Young GC即将触发OC / OU老年代容量和已用量OU持续上涨是内存泄漏的最强信号MC / MU元空间容量和已用量动态生成类场景重点盯这个YGC / YGCTYoung GC次数/累计耗时高频低耗时指向分配速率问题FGC / FGCTFull GC次数/累计耗时FGCT大说明老年代回收成本高或者GC算法配置不当GCTGC累计总耗时全貌指标GCT占运行时间比例过高就必须处理4.4 关于G1和JDK新版本的注意事项如果你的服务运行在JDK8u之后并使用了G1收集器jstat输出的字段含义是完全一样的仍然是S0、S1、E、O这些区域统计。但要注意一个细节G1是一个逻辑上分代但实际上物理内存分区的垃圾回收器它没有传统意义上的连续Eden、Survivor区域jstat显示的百分比是G1各个Region集合的汇总结果。这意味着你会在G1下看到S0、S1经常是0%这是正常的不代表Survivor区没工作而是G1在回收时会切换使用不同的Region作为Survivor区。JDK11之后引入了ZGC、JDK14引入了Shenandoah这些低延迟收集器在jstat输出里依然会显示YGC/FGC等指标但解读方式完全不同。ZGC的GC基本都是并发执行你以为的“FGC0”不代表它没在做回收。所以在用jstat解读新GC算法时建议同时查阅对应收集器的官方说明别拿着G1之前的思维惯性硬套。4.5 踩坑实录jstat不是用来抓线程卡顿的我刚接触jstat时犯过一个方向性错误线上接口突然卡住我第一反应是跑jstat看GC数据。结果GC数据一切正常根本没有Full GC痕迹但问题确实存在。折腾了半天才发现是数据库连接池被耗尽线程都在等连接跟GC毫无关系。这个教训后来让我形成了一个习惯任何JVM问题的排查先做“二分”。先确认是线程问题jstack、内存问题jstatjmap还是资源竞争问题结合监控系统。jstat回答的是“JVM的内存与GC状态”不是“线程为什么卡住”。方向错了再好的工具也是浪费时间。5. 经验总结与工作流建议说了这么多最后分享一点我沉淀下来的工作流习惯希望能帮你把jstat融入日常排查手段里。第一个习惯问题现场先留底。无论是线上突发还是压测发现异常第一步一定是jstat -gcutil PID 1000 10连续采集一组数据连同时间点一起记录下来。哪怕当时看不出端倪后续排查时这份现场数据就是最宝贵的参照系。第二个习惯建立GC基线。服务正常运行时抽空用jstat采集几组数据记录下正常状态的YGC频率、FGC间隔、老年代水位线。日后出了问题拿异常数据跟基线一对比偏离一眼就能看出来。这个成本几乎为零但价值极高。第三个习惯jstat和jmap配合使用。jstat告诉你老年代在涨jmap执行jmap -histo:live PID能进一步告诉你老年代里占比最高的对象类型是什么。如果发现某个业务对象占据了大量内存代码问题基本就有了明确的搜索方向。第四个习惯别忽略服务启动参数。看到jstat数据异常第一时间检查启动参数里-Xms、-Xmx、-XX:NewRatio、-XX:SurvivorRatio这些配置是否合理。很多“GC频繁”的根本原因就是堆大小和比例配错了。确认参数问题后调整重启往往是性价比最高的解决方式。jstat这个工具比它看上去能做的事情多得多。它没有花哨的界面没有炫目的图表输出的只是一行行乍看枯燥的数字。但正是这些数字构成了判断JVM健康与否最可靠的地基。把jstat用熟了你在JVM排查时就不再是靠感觉瞎猜而是手里攥着实打实的数据。下次你的服务再出现诡异的卡顿不妨先别急着上重型分析工具敲一句jstat -gcutil看看它告诉了你什么。
阅读完成 · 觉得有帮助?
咨询建站