ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

jstat命令实战:从GC趋势到JVM内存泄漏排查

jstat命令实战:从GC趋势到JVM内存泄漏排查 凌晨两点被电话吵醒业务群里的告警刷了屏接口 P99 从 50ms 涨到 2sCPU 在容器里顶到了 85%。登录服务器后我没有急着翻日志也没有打开可视化监控平台第一件事是敲了一行命令jstat -gcutil 23441 1000 10这几乎是我排查线上 JVM 问题的固定起手式。jstat 全称 JVM Statistics Monitoring Tool是 JDK 自带的监控命令用来观察 JVM 运行时数据区的状态堆内存分区使用、元空间占用、类加载数量、JIT 编译情况以及 GC 次数和耗时。它零依赖、零侵入不需要在应用里装 Agent不需要开放 JMX 端口一条命令就能拿到 JVM 内部最原始的统计信息。这个工具适合所有跟 Java 应用打交道的人后端开发排查线上卡顿SRE/运维定位 CPU 和内存异常DBA 和服务治理团队做容量评估性能测试同学验证压测过程中的 GC 表现。只要服务器上有 JDK它就在那里等着你不像其他诊断工具那样动不动就需要额外授权。1. 生产环境告警时我为什么先敲 jstat先说一个很多人问过我的问题现在工具这么多Arthas、VisualVM、JFR、PrometheusGrafana 都挺香为什么还死守着 jstat 这个“老古董”因为 jstat 在特定场景下无可替代。它做的是最底层的 JVM 内部统计直接读的就是 HotSpot 的统计计数器和内存池数据。这意味着它没有 Agent 开销、没有指标采集链路延迟、没有监控框架自身的内存消耗。哪怕在线问候业务连跑几万次采样对业务进程的影响也可以忽略不计。而我遇到过的不少可视化监控平台指标来自 JMX Exporter 或 Agent 埋点口径有时候是“近似值”还有可能因为采集频率太低错过瞬息万变的 GC 状态。更关键的是jstat 的输出可以直接对接我的判断逻辑看到 YGC 频率、FGC 次数、Eden 使用率、老年代上涨趋势我基本上心里就有了一张“问题地图”。它能告诉我最近一小时 GC 是不是异常、老年代是不是在稳定爬坡、元空间有没有逼近上限这些信息在最小代价下就给足了。1.1 先分清两个概念JMM 与运行时数据区不少刚转 Java 的同事会把 Java 内存模型JMM和 JVM 运行时数据区搞混。jstat 监控的是后者堆内存、非堆、元空间、GC 统计。JMM 是语言层面的内存可见性规则定义的是 volatile、synchronized 底层的 happens-before 关系跟 jstat 没有任何关系。做 JVM 调优、GC 排查盯的是运行时数据区写并发代码才需要关心 JMM。搞混这两个概念最容易出现的问题就是看到 GC 频繁就以为是 JMM 设计出了问题方向完全带偏。1.2 jstat 与 JDK 版本的关系从 JDK 5 一直到当前的长期支持版本jstat 一直存在且用法基本稳定。我在 JDK 8 和 JDK 17 的容器环境里都用得很顺手。需要知道的是JDK 9 引入模块化后jstat 相关实现被放进了 jdk.jstat 模块对外命令没有任何变化所以网上老教程基本还能直接用。而 JFRJava Flight Recorder这类飞行记录器可以做到毫秒级、带栈信息的完整记录在深度分析上比 jstat 强得多但它属于“事后分析”和 jstat 这种“即时勘察”互补的关系不是替代关系。1.3 哪些典型场景该用 jstat我整理了三个最常遇到的场景基本覆盖了 jstat 的日常高频用途场景典型表现jstat 能给出的线索接口变慢、CPU 居高不下P99 涨、LOAD 偏高YGC/FGC 频率、GC 耗时是否异常频繁 OOM 或内存告警应用重启、内存监控报警各代使用趋势、老年代是否持续上涨发布前后对比新版本上线后表现变差对比 GC 基线判断是不是 GC 配置被影响容量评估压测、扩容前评估实际堆用量、GC 压力决定 -Xmx 是否合理2. 参数拆分与实际用法组合拳才是效率的关键jstat 的命令语法并不复杂但要把它用顺还是有几个容易被忽略的细节。我先从最基本的说起。2.1 基本语法与进程定位语法是这样的jstat -option [-t] [-hlines] vmid [interval [count]]vmid 在本地就是 JVM 进程的 PID。定位 PID 我常用的有三招jps -l 看 Java 进程主类pgrep -f java 配合进程参数ps -ef | grep java 最直观。生产环境里同名应用往往起了多个实例我习惯用 ps -ef | grep java 确认启动参数里的端口或项目名避免抓到不对路的进程。拿 PID 之后最基础的用法是jstat -gc 12345这条命令会打印一次当前堆内存和 GC 统计快照。但只打印一次意义不大后面我会解释为什么。2.2 常用 option 以及它们各自看什么jstat 的 option 可以理解为不同的“查询模式”用 jstat -options 能列出当前 JDK 支持的全部选项。比较常用的有这几个选项输出内容我一般什么时候用-class类加载/卸载数量、耗时怀疑动态代理、反射导致类加载异常时-compilerJIT 编译数量、失败数、耗时刚启动或预热期观察编译压力-gc堆各区域容量、使用量、GC 次数和耗时最常用整体看一眼-gccapacity各区域最小/最大/当前容量确认堆大小配置是否生效-gcutil各区域使用率百分比、GC 次数耗时快速盯盘一屏看清使用率-gccause在 -gcutil 基础上增加最近 GC 原因判断 GC 由谁触发时用-gcnew / -gcold / -gcmetacapacity分区域细看专项分析新生代、老年代、元空间在这些选项里我日常用得最频繁的是 -gcutil 和 -gccause。前者看“用了多少”后者看“为什么 GC”。2.3 采样间隔、采样次数与表头的细节interval 的单位是毫秒count 是采样次数。下面这条命令表示每 1 秒采一次、一共采 10 次jstat -gcutil 12345 1000 10如果不指定 count它会每分钟按 interval 一直打印直到你 CtrlC。我经常在需要“盯一小段时间”的时候用这种持续模式jstat -gc 12345 2000另外两个小参数容易被忽略。-t 会在每行前面打印自 JVM 启动以来的时间戳秒方便你回看输出时对齐业务时间点-h 表示每隔多少行重复打印一次表头比如 -h5 就是每 5 行出一次表头长时间采样时很有用。我习惯写成jstat -gcutil -t -h5 12345 1000 30还有一个 -J 参数它是给 jstat 工具自身传 JVM 参数的。比如 jstat 自己用的默认堆很小在极端场景下可以用 jstat -J-Xmx256m -gc 12345不过绝大多数情况下用不到。2.4 我常用的几个“组合拳”我习惯在服务器上配几条 alias避免每次敲那么长一串alias jvm-gcujstat -gcutil -t -h5 alias jvm-gcjstat -gc -t alias jvm-causejstat -gccause然后配合 Linux 的 watch 命令实现“自动刷新”watch -n 3 jstat -gcutil 12345这样每 3 秒自动刷新一次适合在告警持续期间开着像看仪表盘一样观察各区域使用率变化。3. 输出指标逐列拆解从 S0C 到 GCT 的完整含义不管选哪个 option核心都是要读懂输出列。下面我用最常见的 jstat -gc 输出来讲顺便把 JVM 堆内存的整体结构串一遍。3.1 堆内存的“仓库”模型可以把 JVM 堆想象成一个物流仓库。新生代是“收货区”其中 Eden 是主收货区S0、S1 是两个周转间老年代是“长期存储区”对象存活够久之后搬到这里元空间是数据中心的“档案柜”存类元数据。对象创建出来先进 EdenEden 满了就触发一次 Young GC此时 S0/S1 之间玩“复制”游戏一边是空的一边放存活对象。对象每熬过一次 GC年龄加一达到阈值后晋升到老年代。老年代快满了才触发 Full GC。这套流程理解了jstat 输出的每一列都能对号入座。3.2 jstat -gc 各列含义对照表下面这张表建议收藏我当年就是靠它把 jstat 吃透的列名含义备注S0C / S1CSurvivor 0/1 区当前容量KB两个区通常只有一边有数据S0U / S1USurvivor 0/1 区当前使用量KB经常一个为 0正常EC / EUEden 区当前容量 / 使用量EU 逼近 EC 说明将要 Young GCOC / OU老年代当前容量 / 使用量OU 持续上涨需要警惕MC / MU元空间当前容量 / 使用量动态代理、反射多时关注CCSC / CCSU压缩类空间容量 / 使用量类元数据的一部分一般不用太操心YGC / YGCTYoung GC 次数 / 累计耗时次数是累计值看增速FGC / FGCTFull GC 次数 / 累计耗时每出现一次都值得深挖GCT所有 GC 累计总耗时最终参考指标3.3 单位、累计值与时序陷阱这里有两个坑必须说清楚。第一jstat 输出的容量单位是 KB而且是二进制 1024 进制不是大家习惯的 1000 进制。比如 S0C12288.0 表示 12MB看到一个数字先别急着以为是字节。第二GC 次数和耗时的累计值是从 JVM 启动那一刻开始算的不是实时速率。单独看 YGC3721 没有意义必须看两次采样之间的差值再除以时间间隔才算得出真实的 Young GC 频率。顺带一提GCT 是 JVM GC 计时器给出的总耗时不要死板地要求它永远等于 YGCT FGCT 的和。不同垃圾收集器的统计口径有差异比如 G1 里 mixed GC 的归属就和 Parallel 不一样。遇到数字对不上的情况别慌以 GC 日志为准。3.4 看趋势而不是看单次快照同一个命令单次输出只能说明“此刻用了多少”。真正有价值的分析是连续采样之后对比趋势。比如第一次采到 YGC372110 秒后采到 YGC3727说明 10 秒内发生了 6 次 Young GC平均不到 2 秒一次这种频率对线上服务来说已经很高了。而单独看一次 YGC3727根本看不出频率。同理老年代 OU 如果连续几次采样都在爬而且 Full GC 之后也没有明显回落那就不是简单的“高峰期流量大”很可能是对象没有回收路径也就是泄漏的前兆。4. 三个真实场景用 jstat 判断问题方向纸上谈兵没意思。我给你三个我在排查时经常遇到的场景每一步都对应 jstat 的具体用法。4.1 场景一新生代频繁回收接口卡顿有一类问题是CPU 没有满但接口偶尔出现明显抖动。这时候我通常会怀疑 Young GC 是不是太频繁。先执行jstat -gcutil 12345 1000 10如果输出的 E 列Eden 使用率每次采样都在 95% 以上两三秒内 YGC 次数增加了好几次说明 Eden 区“装满了又清、清了又满”Young GC 在忙个不停。这种形态的典型原因有三个新生代容量设置偏小对象分配速率过高或者有大量超大对象直接撑爆 Eden。接下来我会查启动参数里的 -Xmn以及业务代码里是否有大数组、大集合的批量创建。调整思路通常是扩大新生代、调整 SurvivorRatio或者先优化业务侧的短期大对象分配。4.2 场景二老年代持续上涨疑似内存泄漏另一个高频场景是 Full GC 次数不断增加但老年代使用量不减。我会连续采 30 秒到 1 分钟jstat -gc 12345 500 60重点看两列OU 和 FGC。如果 OU 从 80MB 一路爬到 900MBFull GC 后基本不降或者降一点点又继续涨那就不是“对象暂时堆积”而是有东西被持续留在了老年代。常见嫌疑对象包括全局缓存、静态集合、ThreadLocal 没清理、连接池对象泄漏、以及各种框架层的内部缓存。注意jstat 只能告诉你“老年代在涨”不能告诉你“是谁在涨”。要定位到具体对象必须配合 jmap -histo:live 查看存活对象 Top 类或者抓 heap dump 后用 MAT 分析。这个我在下一章展开。4.3 场景三元空间或类加载异常有些服务大量使用动态代理、CGLIB、反射生成类类加载数量会持续增长。遇到莫名 OOM可以先看类加载方向jstat -class 12345如果 Loaded 的数量持续高且 Unloaded 基本为 0元空间 MC 又逼近 MCMX说明类卸载机制没有正常工作。再配合jstat -compiler 12345看 Failed 编译数是否在增长。如果 Failed 很高往往是字节码生成有问题或者 JIT 编译压力过大。这类问题用 jstat 定位方向很快但后续修复可能需要从框架配置、动态代理策略入手。4.4 一个完整输出示例的手把手分析下面是我根据实际采集习惯整理的一个模拟输出用来给你演示完整分析路径S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 12288.0 12288.0 0.0 1024.0 98304.0 95120.0 262144.0 198400.0 46080.0 34560.0 5120.0 3900.0 3721 48.760 45 61.200 109.960EdenEU/EC 95120/98304 ≈ 96.7%说明 Eden 马上就要满很快发生 Young GC。SurvivorS0U0S1U1024KB这是复制算法正常状态一边空一边有货不用慌。老年代OU198400KB 约 193MBOC262144KB 约 256MB使用率约 75%。关键是看两次采样之间的 OU 增量。YGC3721 次YGCT48.76 秒单次平均约 13ms可接受。FGC45 次FGCT61.2 秒单次平均约 1.36 秒这个 Full GC 单次耗时就相当扎眼了。GCT109.96 秒整体 GC 压力偏大需要查 Full GC 触发原因。看到这里下一手我就直接敲jstat -gccause 12345 1000 3重点看 LGCC最近 GC 原因和 GCC当前 GC 原因。是老年代空间不足、元空间触发、还是代码里显式调用了 System.gc一眼就能区分开。5. 别一个人干活jstat 与 jmap、jstack、jcmd 的配合链路jstat 再能打也只是“看趋势、定方向”的角色。完整的 JVM 问题排查需要一组工具配合。5.1 工具定位矩阵工具核心能力适合场景jstat运行时统计堆、GC、类加载、JIT快速判断 GC 频率、内存趋势jmap堆直方图、heap dump定位大对象、内存泄漏jstack线程快照看死锁、线程阻塞、CPU 热点线程jcmd多功能诊断可替代部分 jmap/jstack一体化采集脚本友好Arthas在线反编译、方法调用追踪、热更新业务代码级别调试JFR JMC毫秒级飞行记录与离线分析低频问题、需要详细证据链时5.2 一套完整的排查链路我常用的链路是这样的。第一步jstat -gccause 明确 GC 频率与触发原因第二步如果怀疑老年代上涨用 jmap -histo:live 看一眼存活对象 Top 类jmap -histo:live 12345 | head -50这一步会输出类的实例数量和占用字节数基本能看出是哪一类对象在堆积。注意-histo:live 这个参数会触发一次 Full GC线上需要谨慎最好在低峰期执行或者直接去掉 :live 看当前堆中的对象统计。第三步如果发现线程相关的异常比如接口不返回用 jstackjstack 12345 thread_dump.txt然后查 BLOCKED、WAITING 状态的线程。第四步如果确认是内存泄漏再抓 heap dumpjmap -dump:formatb,file/data/dump.hprof 12345dump 文件用 MAT 或 JProfiler 离线分析。这套组合拳下来从“GC 频繁”到“具体是哪段代码泄漏”就能完整闭环了。5.3 什么情况下该上更重的工具如果你的问题“时有时无”jstat 盯半小时也没抓到那就别死磕实时采集了。直接上 JFRjcmd 12345 JFR.start duration60s filename/tmp/recording.jfr跑完用 JMC 打开里面有完整的 GC 明细、分配采样、线程活动和锁竞争信息。JFR 的侵入性远小于 Arthas 的 attach Agent也适合在生产环境长时间开JDK 11 默认支持。jstat 的成本低、上手快但遇到“低频但致命”的疑难杂症还是要会用更专业的记录工具。6. 用 jstat 必须知道的坑这里总结几个我踩过、或者看同事踩过的坑。每一条都是真实会发生的而不是纸上推演。6.1 累计值陷阱GC 次数从 JVM 启动开始累计不等于实时速率。之前有人拿线上 JVM 的 YGC 数很大就当事故其实是服务已运行 20 天。一定要看两次采样之间的差值除以间隔时间。而且采样间隔建议拉长到 10 秒以上个别 1 秒采一次的受 GC 周期影响波动大很容易误判。我的建议是先用 5000ms 间隔采 6 次再根据数据决定要不要加密。6.2 垃圾收集器不同统计口径不同Parallel、CMS、G1、ZGC 在 jstat 里的 YGC/FGC 含义不完全一致。G1 的 mixed GC 和完整 GC cycle 的归属在不同版本里表现都有差异ZGC 和 Shenandoah 由于主要是并发收集jstat 显示的 FGC 次数可能长期不涨这不是“没 GC”而是收集形态不同。千万别拿 ZGC 的 jstat 输出和 Parallel 的 jstat 输出直接对比很容易得出完全错误的结论。跨收集器对比时以 GC 日志和 JFR 数据为准。6.3 容器环境下的堆大小与感知容器里跑 Java 有个经典问题如果 JDK 版本低于 8u191或者没有显式开启 UseContainerSupportJVM 会把宿主机内存当成可用内存来计算默认堆大小导致 -Xmx 没生效、堆上限远超容器配额。这时候 jstat 输出的 OC、EC 会大得离谱GC 又频繁又长。所以拿到 jstat 数据后先看容量列与容器内存 limit 是否匹配再谈调优。6.4 jstatd 远程监控的安全边界jstat 默认只支持本机 PID。想跨机器远程执行需要启动 jstatd 并配置 RMI 端口和安全策略文件。但生产环境开放 RMI 服务有明确的安全风险具体来说就是端口暴露面太大、没有内置鉴权很容易被扫描和利用。我的建议是能本地执行就本地执行远程监控走 JMX Prometheus或者直接 JFR 录制后离线分析。非要远程用 jstatd也要绑内网 IP、限制来源、跑在独立低权限账号下。6.5 版本差异与输出格式的细微变化最后提一个容易被忽视的在 JDK 9 之后同一段命令在模块化应用上输出列几乎没有变化但某些非堆区域如果未分配-gcnew、-gcold 这类专项命令可能会输出 0 或空值不要一看到 0 就以为出了问题先用 -gccapacity 确认对应区域是否有实际容量配置。另外在容器里如果拿到的是 PID 1 的 Java 进程jps 有时候会识别不到应有的进程名我是靠 ps 的二次确认来兜底的。最后分享一个我自己的习惯每次服务发布稳定后我都会立刻采一组基线数据比如 jstat -gc 服务PID 1000 5把 YGC 频率、FGC 次数、GCT 记到变更记录里。下次出问题先对比基线是“突然变差”还是“一直就快”一眼就清楚。JVM 调优没有银弹但 jstat 这个起手式确实能帮你省下大把抓瞎的时间。
返回列表