news 2026/10/28 11:09:11

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
jstat命令实战:从GC趋势到JVM内存泄漏排查

凌晨两点被电话吵醒,业务群里的告警刷了屏:接口 P99 从 50ms 涨到 2s,CPU 在容器里顶到了 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、Prometheus+Grafana 都挺香,为什么还死守着 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 模块,对外命令没有任何变化,所以网上老教程基本还能直接用。而 JFR(Java 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] [-h<lines>] <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 一直打印,直到你 Ctrl+C。我经常在需要“盯一小段时间”的时候用这种持续模式:

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-gcu='jstat -gcutil -t -h5' alias jvm-gc='jstat -gc -t' alias jvm-cause='jstat -gccause'

然后配合 Linux 的 watch 命令实现“自动刷新”:

watch -n 3 'jstat -gcutil 12345'

这样每 3 秒自动刷新一次,适合在告警持续期间开着,像看仪表盘一样观察各区域使用率变化。

3. 输出指标逐列拆解:从 S0C 到 GCT 的完整含义

不管选哪个 option,核心都是要读懂输出列。下面我用最常见的 jstat -gc 输出来讲,顺便把 JVM 堆内存的整体结构串一遍。

3.1 堆内存的“仓库”模型

可以把 JVM 堆想象成一个物流仓库。新生代是“收货区”,其中 Eden 是主收货区,S0、S1 是两个周转间;老年代是“长期存储区”,对象存活够久之后搬到这里;元空间是数据中心的“档案柜”,存类元数据。

对象创建出来先进 Eden,Eden 满了就触发一次 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 GC
OC / OU老年代当前容量 / 使用量OU 持续上涨需要警惕
MC / MU元空间当前容量 / 使用量动态代理、反射多时关注
CCSC / CCSU压缩类空间容量 / 使用量类元数据的一部分,一般不用太操心
YGC / YGCTYoung GC 次数 / 累计耗时次数是累计值,看增速
FGC / FGCTFull GC 次数 / 累计耗时每出现一次都值得深挖
GCT所有 GC 累计总耗时最终参考指标

3.3 单位、累计值与时序陷阱

这里有两个坑必须说清楚。第一,jstat 输出的容量单位是 KB,而且是二进制 1024 进制,不是大家习惯的 1000 进制。比如 S0C=12288.0 表示 12MB,看到一个数字先别急着以为是字节。第二,GC 次数和耗时的累计值是从 JVM 启动那一刻开始算的,不是实时速率。单独看 YGC=3721 没有意义,必须看两次采样之间的差值,再除以时间间隔,才算得出真实的 Young GC 频率。

顺带一提,GCT 是 JVM GC 计时器给出的总耗时,不要死板地要求它永远等于 YGCT + FGCT 的和。不同垃圾收集器的统计口径有差异,比如 G1 里 mixed GC 的归属就和 Parallel 不一样。遇到数字对不上的情况,别慌,以 GC 日志为准。

3.4 看趋势,而不是看单次快照

同一个命令,单次输出只能说明“此刻用了多少”。真正有价值的分析是连续采样之后对比趋势。比如第一次采到 YGC=3721,10 秒后采到 YGC=3727,说明 10 秒内发生了 6 次 Young GC,平均不到 2 秒一次,这种频率对线上服务来说已经很高了。而单独看一次 YGC=3727,根本看不出频率。

同理,老年代 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 一路爬到 900MB,Full 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.960
  • Eden:EU/EC = 95120/98304 ≈ 96.7%,说明 Eden 马上就要满,很快发生 Young GC。
  • Survivor:S0U=0,S1U=1024KB,这是复制算法正常状态,一边空一边有货,不用慌。
  • 老年代:OU=198400KB 约 193MB,OC=262144KB 约 256MB,使用率约 75%。关键是看两次采样之间的 OU 增量。
  • YGC:3721 次,YGCT=48.76 秒,单次平均约 13ms,可接受。
  • FGC:45 次,FGCT=61.2 秒,单次平均约 1.36 秒,这个 Full GC 单次耗时就相当扎眼了。
  • GCT=109.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 看当前堆中的对象统计。

第三步,如果发现线程相关的异常,比如接口不返回,用 jstack:

jstack 12345 > thread_dump.txt

然后查 BLOCKED、WAITING 状态的线程。第四步,如果确认是内存泄漏,再抓 heap dump:

jmap -dump:format=b,file=/data/dump.hprof 12345

dump 文件用 MAT 或 JProfiler 离线分析。这套组合拳下来,从“GC 频繁”到“具体是哪段代码泄漏”就能完整闭环了。

5.3 什么情况下该上更重的工具

如果你的问题“时有时无”,jstat 盯半小时也没抓到,那就别死磕实时采集了。直接上 JFR:

jcmd 12345 JFR.start duration=60s 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,或者没有显式开启 UseContainerSupport,JVM 会把宿主机内存当成可用内存来计算默认堆大小,导致 -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 这个起手式,确实能帮你省下大把抓瞎的时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 0:47:43

SourceCodePro编程字体安装与跨平台配置指南

简介&#xff1a;Source Code Pro 是 Adobe 官方开源的等宽编程字体&#xff0c;专为代码阅读与编写优化&#xff0c;适合各类程序员在 IDE、终端、文档编辑等场景中长期使用。本资源提供完整字体家族&#xff0c;涵盖 Regular、Bold、Light、Semibold、Black 等 9 种字重及斜体…

作者头像 李华
网站建设 2026/10/17 5:16:32

74LS175四人抢答器设计:硬件级实时仲裁原理与工程实践

1. 这不是玩具电路&#xff0c;是数字逻辑的“裁判员”——为什么四人抢答必须用74LS175&#xff1f;你拆过教室里那个按下去就亮灯、还带蜂鸣的抢答器吗&#xff1f;或者见过知识竞赛现场&#xff0c;四位选手手悬在按钮上方、主持人话音未落就有人猛拍桌面——结果只有一路信…

作者头像 李华
网站建设 2026/10/27 23:55:07

Uzi获评世界第一ADC背后:英雄联盟挂机封号新规与排位环境治理

今天份的每日一撸&#xff0c;聊两件事&#xff1a;一是GRF那边有人把Uzi评成了世界第一ADC&#xff0c;二是官方放了个狠招——恶意挂机要直接封号。说实话&#xff0c;这两条放在同一天出现还挺有意思的。前者是职业赛场上对个人能力的最高认可&#xff0c;后者是排位环境里对…

作者头像 李华
网站建设 2026/10/23 19:43:02

Unity角色动画系统搭建指南:从绑定到运镜的完整流程

1. 从零搭建角色动画系统的整体思路角色动画这件事&#xff0c;刚上手的时候很容易陷入一个误区&#xff1a;拿到模型就急着往场景里拖&#xff0c;然后发现骨骼对不上、动画播不了、镜头跟得头晕。我在前几个项目里踩过不少这类坑&#xff0c;后来才慢慢总结出一套相对稳定的流…

作者头像 李华
网站建设 2026/10/17 8:11:00

Godot编辑器移植鸿蒙PC的可行性分析与技术拆解

如果你和我一样&#xff0c;平时主要用 Godot 写游戏&#xff0c;最近一定绕不开一个问题&#xff1a;哪天主力机器切换到鸿蒙 PC&#xff0c;我手上的 Godot 编辑器还能不能原生跑起来&#xff1f;这不算空想。开源鸿蒙的 x86_64 镜像如今已经越来越容易装进普通 PC&#xff0…

作者头像 李华
网站建设 2026/10/16 13:47:58

用虾聊(xialiao.ai)搭建AI应用容错架构:超时、重试与熔断实战

先泼一盆冷水&#xff1a;任何把“国内版 Moltbook”挂在标题前面的内容&#xff0c;我建议你都先缓一缓再点。Moltbook 本身是个有意思的参考项目&#xff0c;但硬要在它前面加个“国内版”&#xff0c;十有八九是拿开源概念做营销包装&#xff0c;真正要交付的其实是自己的那…

作者头像 李华