JVM - 监控及诊断工具cmd
# 1. 概述:为何需要命令行工具?
软件性能诊断是工程师日常工作中不可或缺的一环。尤其在追求极致用户体验的今天,高效解决应用性能问题能带来显著的业务价值。Java 作为广泛应用的编程语言,其性能诊断一直备受关注。导致 Java 应用性能问题的原因多种多样,如线程管理不当、磁盘 I/O 瓶颈、数据库访问缓慢、网络延迟、垃圾收集(GC)效率低下等。要精准定位这些问题,掌握并善用性能诊断工具至关重要。
核心理念:
- 数据驱动:用具体的数据指标来发现和量化问题。
- 知识分析:结合 JVM 原理和应用知识来分析数据背后的原因。
- 工具辅助:利用专业的工具来高效地收集数据和处理问题。
- 监控先行:没有监控就没有调优的基础。
基础命令行工具概览
除了我们最熟悉的 javac
(编译) 和 java
(运行) 命令,JDK 的 bin
目录下还包含了一系列用于监控和诊断 JVM 的命令行工具。这些工具能帮助我们获取目标 JVM 在不同方面、不同层级的详细信息,有效解决 Java 应用程序的疑难杂症。
图:JDK bin 目录下的部分命令行工具
这些工具的官方源码可以参考 OpenJDK 的仓库,例如 JDK 11 的路径:
http://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/jdk.jcmd/share/classes/sun/tools
# 2. jps:轻量级 JVM 进程状态查看器
jps
(Java Process Status) 命令用于显示指定系统内当前正在运行的所有 HotSpot 虚拟机进程。它可以快速列出 Java 进程的本地虚拟机唯一 ID (LVMID),以及进程启动的主类名或 Jar 文件名。
重要说明:对于本地运行的 Java 虚拟机进程,其 LVMID 与操作系统分配的进程 ID (PID) 是一致且唯一的。这使得我们可以方便地将 jps
的输出与其他需要 PID 的命令(如 jstat
, jmap
, jstack
)关联起来。
基本语法:
jps [options] [hostid]
代码示例:
假设我们运行以下简单的 Java 程序,它会等待用户输入:
import java.util.Scanner;
/**
* 一个简单的 Java 程序,用于演示 jps 命令。
* 程序启动后会阻塞,等待控制台输入。
*/
public class ScannerTest {
public static void main(String[] args) {
System.out.println("ScannerTest 正在运行,等待输入...");
Scanner scanner = new Scanner(System.in);
// next() 方法会阻塞,直到用户在控制台输入内容并按下回车
String info = scanner.next();
System.out.println("输入内容: " + info);
scanner.close();
System.out.println("ScannerTest 运行结束。");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
运行该程序后,在另一个命令行窗口中执行 jps
:
jps
输出可能类似:
图:jps 命令的基本输出
这里 20472
就是 ScannerTest
进程的 LVMID (也是 PID),ScannerTest
是启动的主类名。19812 Jps
是 jps
命令自身的进程。
options
参数详解:
jps
支持多个选项来显示更详细的信息:
-q
(Quiet): 仅显示 LVMID,省略主类名、Jar 文件名和传递给main
方法的参数。适用于只需要进程 ID 的场景。-l
(Long): 输出应用程序主类的全限定名;如果进程是通过-jar
参数执行的 Jar 包,则输出 Jar 文件的完整路径。-m
(Main arguments): 输出传递给主类main()
方法的参数。-v
(Verbose / VM arguments): 列出虚拟机进程启动时显式指定的 JVM 参数(例如-Xms
,-Xmx
,-XX:NewRatio
等)。
参数组合使用示例:
jps -l -m -v
输出可能类似:
图:jps -l -m -v 参数组合使用示例
注意:如果目标 Java 进程启动时使用了 -XX:-UsePerfData
参数关闭了性能统计数据的收集,那么 jps
(以及后续介绍的 jstat
)将无法探测到该 Java 进程。
hostid
参数:
hostid
用于指定远程主机的信息,格式通常是 [protocol:][[//]hostname][:port][/servername]
。要实现远程监控,远程主机上需要运行 jstatd
服务。
- 安全考虑:在对安全性要求较高的网络环境中,直接暴露
jstatd
可能存在风险。可以配置 JMX 安全策略文件来限制访问,但这可能受到 IP 欺诈等攻击。最安全的方式通常是在目标主机上本地执行jps
和jstat
,而不是运行jstatd
提供远程访问。
# 3. jstat:深入 JVM 运行时统计信息
官方文档参考 (Java 8):
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
jstat
(JVM Statistics Monitoring Tool) 是一个强大的命令行工具,用于实时监视 Java 虚拟机(JVM)的各种运行时状态信息。它可以显示本地或远程 JVM 进程中的**类加载、内存(堆、元空间)、垃圾收集(GC)以及即时编译(JIT)**等详细的运行数据。
在没有图形用户界面(GUI)的环境下(例如纯文本控制台的服务器),jstat
是定位 JVM 运行时性能问题(特别是 GC 问题和内存泄漏)的首选工具。
基本语法:
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
<option>
: 指定要查询的统计信息类型(详见下文)。-t
: 在输出的第一列显示时间戳(从 JVM 启动开始的秒数)。-h<lines>
: 每输出lines
行数据后,重新打印一次表头。<vmid>
: 目标 Java 进程的 LVMID(即jps
输出的进程 ID)。<interval>
: 连续输出的时间间隔,单位是毫秒(ms)或秒(s)。例如1000ms
或1s
。<count>
: 总共输出的次数。如果省略count
但指定了interval
,jstat
会持续输出,直到目标 JVM 进程终止或手动停止命令。
可以通过 jstat -h
或 jstat -help
查看所有可用选项。
option
参数详解:
jstat
的核心在于其丰富的 option
参数,用于指定不同的监控维度:
类加载相关 (-class
)
-class
: 显示类加载器(ClassLoader)的相关统计信息。Loaded
: 已加载的类的数量。Bytes
: 已加载类占用的总空间(字节)。Unloaded
: 已卸载的类的数量。Bytes
: 已卸载类释放的总空间(字节)。Time
: 类加载和卸载操作所消耗的时间(秒)。
示例:查看进程
13968
的类加载信息:jstat -class 13968
1
JIT 编译相关 (-compiler
, -printcompilation
)
-compiler
: 显示 HotSpot JIT 编译器编译活动的相关统计信息。Compiled
: 已完成编译任务的数量。Failed
: 编译失败的任务数量。Invalid
: 无效的编译任务数量。Time
: 编译任务消耗的时间(秒)。FailedType
: 最后一次失败编译的类型。FailedMethod
: 最后一次失败编译的方法名。
示例:查看进程
13968
的 JIT 编译统计:jstat -compiler 13968
1-printcompilation
: 输出最近被 JIT 编译过的方法信息。Compiled
: 最近编译的方法数量。Size
: 方法字节码的大小。Type
: 编译类型。Method
: 方法名(类名.方法名)。
示例:查看进程
13968
最近编译的方法:jstat -printcompilation 13968
1
垃圾收集 (GC) 相关 (重点)
这是 jstat
最常用的功能,有多个选项可以从不同角度观察 GC 和内存情况。
-gc
: 显示堆内存各个区域的容量、已使用空间以及 GC 的统计信息。这是最常用的 GC 监控选项之一。示例代码 (
GCTest.java
):一个不断创建对象的程序,用于模拟内存增长和 GC。import java.util.ArrayList; /** * 用于演示 jstat -gc* 相关命令的示例程序。 * 不断创建 byte 数组对象并添加到 List 中,模拟内存占用增长,可能触发 GC。 */ public class GCTest { public static void main(String[] args) { ArrayList<byte[]> list = new ArrayList<>(); for (int i = 0; i < 1000; i++) { // 每次循环创建一个 100KB 大小的 byte 数组 byte[] arr = new byte[1024 * 100]; list.add(arr); try { // 短暂休眠,模拟应用运行过程中的停顿 Thread.sleep(120); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("GCTest 执行完毕"); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24启动 JVM 参数:限制堆大小为 60MB。
-Xms60m -Xmx60m -XX:SurvivorRatio=8
1执行
jstat -gc
命令:假设GCTest
进程 ID 为13968
,每秒输出一次,共输出 10 次。jstat -gc 13968 1000 10 # 或者 jstat -gc 13968 1s 10
1
2输出解读:
图:jstat -gc 输出示例
表头 含义 (单位: KB) 解释 S0C
当前 Survivor 0 区的容量 (Capacity) S1C
当前 Survivor 1 区的容量 (Capacity) S0U
Survivor 0 区已使用的大小 (Utilization) S1U
Survivor 1 区已使用的大小 (Utilization) EC
当前 Eden 区的容量 (Capacity) EU
Eden 区已使用的大小 (Utilization) 观察 EU 的增长和突然减少,可以判断 Minor GC 的发生 OC
当前 Old (老年代) 区的容量 (Capacity) OU
Old (老年代) 区已使用的大小 (Utilization) 观察 OU 的增长,判断对象是否晋升到老年代,持续增长可能意味着内存泄漏 MC
Metaspace (元空间) 的容量 (Capacity) (JDK 8 及以后) MU
Metaspace (元空间) 已使用的大小 (Utilization) CCSC
压缩类空间 (Compressed Class Space) 的容量 (Capacity) (如果启用指针压缩) CCSU
压缩类空间已使用的大小 (Utilization) YGC
从 JVM 启动至今,Young GC (Minor GC) 的次数 YGCT
从 JVM 启动至今,Young GC (Minor GC) 的总耗时 (秒) 结合 YGC 次数,可以计算平均单次 Minor GC 时间 FGC
从 JVM 启动至今,Full GC 的次数 FGC 次数频繁是严重的性能问题 FGCT
从 JVM 启动至今,Full GC 的总耗时 (秒) 单次 Full GC 耗时长会导致应用长时间卡顿 (STW) GCT
从 JVM 启动至今,所有 GC (YGC + FGC) 的总耗时 (秒) -gccapacity
: 与-gc
类似,但主要关注堆各个区域的最小容量 (Min)、最大容量 (Max) 和当前容量 (Current)。还包括 GC 次数统计。有助于了解堆内存的配置和动态调整情况。示例:
jstat -gccapacity 13968
1图:jstat -gccapacity 输出示例 (NGCMN: 年轻代最小容量, NGCMX: 年轻代最大容量, NGC: 当前年轻代容量, OGCMN: 老年代最小容量, OGCMX: 老年代最大容量, OGC: 当前老年代容量, MCMN: 元空间最小容量, MCMX: 元空间最大容量, MC: 当前元空间容量, CCSMN/CCSMC/CCSC: 压缩类空间相关)
-gcutil
: 与-gc
类似,但输出的是各区域已使用空间占总容量的百分比。更直观地了解内存使用率。示例:
jstat -gcutil 13968 1s 5
1图:jstat -gcutil 输出示例
表头 含义 (百分比) S0
Survivor 0 区已使用空间百分比 S1
Survivor 1 区已使用空间百分比 E
Eden 区已使用空间百分比 O
Old (老年代) 区已使用空间百分比 M
Metaspace (元空间) 已使用空间百分比 CCS
压缩类空间已使用空间百分比 YGC
Young GC 次数 YGCT
Young GC 总耗时 (秒) FGC
Full GC 次数 FGCT
Full GC 总耗时 (秒) GCT
GC 总耗时 (秒) -gccause
: 功能与-gcutil
基本相同,但额外输出最后一次 GC 或当前正在发生的 GC 的原因 (LGCC
- Last GC Cause,GCC
- Current GC Cause)。有助于分析 GC 触发的具体场景(例如:Allocation Failure, System.gc(), Metadata GC Threshold 等)。示例:
jstat -gccause 13968
1图:jstat -gccause 输出示例 (注意多了 LGCC 和 GCC 列)
-gcnew
: 专注于新生代的 GC 状况。包括 S0/S1/Eden 区的容量、使用量,以及对象晋升到老年代的年龄 (TT
,MTT
) 和期望的 Survivor 空间占用 (DSS
)。示例:
jstat -gcnew 13968
1-gcnewcapacity
: 与-gcnew
类似,但主要关注新生代各区域的最小、最大和当前容量。示例:
jstat -gcnewcapacity 13968
1-gcold
: 专注于老年代的 GC 状况。包括老年代和元空间的容量、使用量以及 GC 次数和耗时。示例:
jstat -gcold 13968
1-gcoldcapacity
: 与-gcold
类似,但主要关注老年代的最小、最大和当前容量。示例:
jstat -gcoldcapacity 13968
1-gcpermcapacity
(JDK 7及之前): 显示永久代 (Permanent Generation) 的容量信息。在 JDK 8 及之后被元空间 (Metaspace) 取代,此选项可能不再适用或输出内容变化。
其他通用参数:
interval
: 输出统计数据的时间间隔,单位毫秒或秒。count
: 指定总共查询的次数。-t
: 在输出的第一列增加一个时间戳 (Timestamp),表示从 JVM 启动到当前采样点的时间(秒)。- 应用:结合 GCT(总 GC 时间)列,可以计算 GC 时间占总运行时间的比例。例如,比较两次采样点的 Timestamp 差值(运行时间增量)和 GCT 差值(GC 时间增量)。如果 GC 时间占比过高(如经验值超过 20%),说明堆内存压力较大;如果超过 90%,则可能即将 OOM。
示例 (
-gcutil
结合-t
):jstat -gcutil -t 13968 1s 5
1图:jstat -gcutil -t 输出示例 (第一列为 Timestamp)
-h<lines>
: 在连续输出时,每隔lines
行数据就重新打印一次表头,方便阅读。示例 (
-gcutil
结合-t
和-h3
):每 3 行数据输出一次表头。jstat -gcutil -t -h3 13968 1s 10
1图:jstat -h 参数示例
利用 jstat 判断内存泄漏
jstat
可以辅助初步判断是否存在内存泄漏:
- 获取基线:在 Java 程序稳定运行一段时间后,执行
jstat -gc <vmid> <interval> <count>
(例如jstat -gc 13968 5s 12
,观察一分钟)。记录下这组数据中OU
(老年代已使用空间) 列的最小值。 - 长期观察:每隔一段较长的时间(例如几小时或一天),重复步骤 1,获取多组
OU
的最小值。 - 趋势分析:如果这些
OU
的最小值呈现持续上涨的趋势,即使经过了多次 Full GC (观察FGC
次数增加) 也无法降下来,那么很可能存在内存泄漏——即不再使用的对象仍然被引用,无法被 GC 回收,导致老年代内存持续增长。
# 4. jinfo:实时查看与调整 JVM 参数
jinfo
(Configuration Info for Java) 命令用于查看目标 Java 进程正在使用的 JVM 配置参数,并且可以在运行时动态地修改部分参数并使其立即生效。
用途:
- 确认 JVM 当前实际使用的参数值(包括默认值和用户指定的)。
- 在不重启服务的情况下,临时调整某些可管理的(manageable)JVM 参数。
基本语法:
jinfo [option] <pid>
必须提供目标 Java 进程的 PID。
option
参数详解:
选项 | 选项说明 |
---|---|
(无选项) | 输出目标 JVM 当前加载的所有配置参数 (VM Flags) 和 Java 系统属性 (System.getProperties() ) |
-flag <name> | 输出指定名称的 JVM 参数 (<name> ) 的当前值。 |
-flag [+|-]<name> | 动态启用 (+) 或禁用 (-) 指定名称的 可管理 (manageable) JVM 参数。 |
-flag <name>=<value> | 动态修改指定名称的 可管理 (manageable) JVM 参数的值。 |
-flags | 仅输出目标 JVM 当前生效的所有配置参数 (VM Flags)。 |
-sysprops | 仅输出目标 JVM 当前的 Java 系统属性 (System.getProperties() )。 |
示例:
假设 GCTest
进程 ID 为 13968
。
查看所有系统属性:
jinfo -sysprops 13968
1输出可能包含
java.version
,os.name
,user.dir
等大量系统属性。# 部分输出示例 jboss.modules.system.pkgs = com.intellij.rt java.vendor = Oracle Corporation sun.java.launcher = SUN_STANDARD sun.management.compiler = HotSpot 64-Bit Tiered Compilers catalina.useNaming = true os.name = Windows 10 ...
1
2
3
4
5
6
7
8查看所有 VM Flags:
jinfo -flags 13968
1输出会列出所有非默认的 JVM 参数以及命令行参数。
# 输出示例 Attaching to process ID 13968, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.231-b11 Non-default VM flags: -XX:CICompilerCount=4 -XX:InitialHeapSize=62914560 -XX:MaxHeapSize=62914560 -XX:MaxNewSize=20971520 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=20971520 -XX:OldSize=41943040 -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC Command line: -Xms60m -Xmx60m -XX:SurvivorRatio=8 -javaagent:F:\Programming area\Ideal\IntelliJ IDEA 2020.3\lib\idea_rt.jar=55919:F:\Programming area\Ideal\IntelliJ IDEA 2020.3\bin -Dfile.encoding=UTF-8
1
2
3
4
5
6
7查看特定 Flag 的值:
# 查看是否启用了 ParallelGC (Parallel Scavenge + Parallel Old) jinfo -flag UseParallelGC 13968 # 输出: -XX:+UseParallelGC (表示已启用) # 查看是否启用了 G1 GC jinfo -flag UseG1GC 13968 # 输出: -XX:-UseG1GC (表示未启用)
1
2
3
4
5
6
7动态修改可管理 Flag (以
PrintGCDetails
为例,它通常是 manageable 的):# 1. 启用 PrintGCDetails jinfo -flag +PrintGCDetails 13968 # 2. 验证是否已启用 jinfo -flag PrintGCDetails 13968 # 输出: -XX:+PrintGCDetails # 3. 禁用 PrintGCDetails jinfo -flag -PrintGCDetails 13968 # 4. 验证是否已禁用 jinfo -flag PrintGCDetails 13968 # 输出: -XX:-PrintGCDetails
1
2
3
4
5
6
7
8
9
10
11
12
13注意:并非所有参数都支持运行时修改。只有被 JVM 标记为
manageable
的参数才可以。GC 策略相关的参数(如UseG1GC
)通常不是manageable
的,不能在运行时更改。
扩展:查找 JVM 参数信息
除了 jinfo
,还可以用 java
命令本身来查看 JVM 参数信息:
java -XX:+PrintFlagsInitial
: 打印所有 JVM 参数的初始默认值。# 部分输出示例 [Global flags] intx ActiveProcessorCount = -1 {product} uintx AdaptiveSizeDecrementScaleFactor = 4 {product} uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} uintx AdaptiveSizePausePolicy = 0 {product} ...
1
2
3
4
5
6
7java -XX:+PrintFlagsFinal
: 打印所有 JVM 参数在加载用户配置后的最终生效值。如果值与默认值不同,会用:=
标出。# 部分输出示例 [Global flags] intx ActiveProcessorCount = -1 {product} ... intx CICompilerCount := 4 {product} # 被修改过的值 uintx InitialHeapSize := 333447168 {product} # 被修改过的值 uintx MaxHeapSize := 1029701632 {product} # 被修改过的值 uintx MaxNewSize := 1774714880 {product}
1
2
3
4
5
6
7
8java -XX:+PrintCommandLineFlags
: 打印出由用户显式设置或 JVM 根据环境自动调整的-XX
参数及其值。这对于了解哪些参数影响了最终配置很有帮助。# 输出示例 -XX:InitialHeapSize=332790016 -XX:MaxHeapSize=5324640256 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
1
2查找
manageable
参数:可以通过PrintFlagsFinal
结合grep
(或 Windowsfindstr
) 来查找哪些参数是可以在运行时修改的。# Linux/macOS java -XX:+PrintFlagsFinal -version | grep manageable # Windows java -XX:+PrintFlagsFinal -version | findstr manageable
1
2
3
4
5
# 5. jmap:导出堆转储快照与分析内存使用
官方文档参考 (Java 11):
https://docs.oracle.com/en/java/javase/11/tools/jmap.html
jmap
(JVM Memory Map) 是一个非常重要的工具,主要有两个核心功能:
- 生成堆转储快照 (Heap Dump):将某一时刻 JVM 堆内存中的对象信息完整地保存到一个二进制文件中(通常以
.hprof
结尾)。这是分析内存泄漏、查找大对象、诊断 OOM 等内存问题的最权威依据。 - 查询内存相关信息:获取目标 Java 进程的堆内存使用情况、对象统计信息、类加载信息等。
基本语法:
jmap [option] <pid> # 本地进程
jmap [option] <executable> <core> # 对核心转储文件操作
jmap [option] [server_id@]<remote-hostname-or-IP> # 远程进程 (需 JMX 或 jstatd 支持)
2
3
option
参数详解:
选项 | 作用 | 平台限制 |
---|---|---|
-dump:<options> | 生成堆转储快照 (heap dump)。常用子选项:live : 只 dump 堆中的存活对象 (推荐,文件更小,分析更快)。format=b : 指定输出为二进制格式。file=<filename> : 指定 dump 文件名。 | 通用 |
-heap | 输出 Java 堆的详细信息,包括使用的 GC 算法、堆配置参数 (Min/Max Heap Size, NewRatio 等)、各区域 (Eden, Survivor, Old) 的容量和使用情况。 | 通用 |
-histo[:live] | 输出堆中对象的统计直方图。按类名、实例数量、总占用字节数排序。添加 :live 只统计存活对象。非常适合快速查找占用内存最多的对象类型。 | 通用 |
-finalizerinfo | 显示在 F-Queue 中等待 Finalizer 线程执行 finalize() 方法的对象信息。 | Linux/Solaris |
-permstat | (JDK 7及之前) 以 ClassLoader 为统计口径,输出永久代 (PermGen) 的内存状态信息。 | Linux/Solaris |
-F | 强制模式。当目标 JVM 进程对正常的 -dump 请求没有响应时(例如进程挂起),可以尝试使用此选项强制生成 dump 文件。可能导致 dump 文件不完整或不一致。 | Linux/Solaris |
-J<flag> | 向运行 jmap 的 JVM 传递参数,例如 -J-Xmx512m 。 | 通用 |
-h | -help | 显示 jmap 的帮助信息。 | 通用 |
核心用法一:导出堆转储快照 (Heap Dump)
手动导出:
# 导出所有对象 (包括已死亡但未回收的) 到文件 3.hprof jmap -dump:format=b,file=d:\3.hprof 13968 # 仅导出存活对象 (推荐) 到文件 4.hprof jmap -dump:live,format=b,file=d:\4.hprof 13968
1
2
3
4
5图:使用 jmap -dump 命令
执行后会在 D 盘生成 dump 文件。通常
live
模式生成的 dump 文件会比全量 dump 小,尤其是在 GC 刚发生后。图:生成的 dump 文件
自动导出 (OOM 时): 在 JVM 启动参数中配置,可以在发生
OutOfMemoryError
时自动生成 dump 文件,非常适合生产环境排查问题。-XX:+HeapDumpOnOutOfMemoryError
: 启用 OOM 时自动 dump。-XX:HeapDumpPath=<path/to/filename.hprof>
: 指定 dump 文件的生成路径和文件名。可以使用%p
占位符表示进程 ID。
示例代码 (
GCTest.java
- 可能导致 OOM):import java.util.ArrayList; /** * 持续分配内存,可能导致 OOM,用于演示自动 Heap Dump。 */ public class GCTest { public static void main(String[] args) { ArrayList<byte[]> list = new ArrayList<>(); System.out.println("GCTest 开始分配内存..."); try { for (int i = 0; i < 1000; i++) { // 循环次数可能需要调整以确保 OOM byte[] arr = new byte[1024 * 100]; // 100KB list.add(arr); Thread.sleep(60); // 稍微减慢分配速度 } } catch (OutOfMemoryError oom) { System.err.println("发生 OutOfMemoryError!"); // oom.printStackTrace(); // 可以选择性打印堆栈 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("GCTest 执行结束。"); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24启动 JVM 参数:限制堆大小并启用自动 Dump。
-Xms60m -Xmx60m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\oom_dump_%p.hprof
1运行程序,当发生 OOM 时,控制台会打印错误,并且在 D 盘会自动生成类似
oom_dump_xxxxx.hprof
的文件。图:OOM 时自动生成的 dump 文件
核心用法二:显示堆内存相关信息
jmap -heap <pid>
: 查看堆的整体配置和使用情况。jmap -histo <pid>
: 查看堆中对象的直方图统计。
示例:
# 查看进程 1904 的堆信息,并将结果重定向到 a.txt
jmap -heap 1904 > a.txt
# 查看进程 1904 的对象直方图,并将结果重定向到 b.txt
jmap -histo 1904 > b.txt
2
3
4
5
a.txt
(-heap
输出) 内容解读:
Attaching to process ID 1904, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.231-b11
// 使用的内存分配方式
using thread-local object allocation.
// 使用的 GC 收集器 (这里是 Parallel GC,包含 Parallel Scavenge + Parallel Old)
Parallel GC with 10 thread(s)
// ---- 堆配置信息 (Heap Configuration) ----
Heap Configuration:
MinHeapFreeRatio = 0 // 最小堆空闲比例
MaxHeapFreeRatio = 100 // 最大堆空闲比例
MaxHeapSize = 4255121408 (4058.0MB) // 最大堆内存 (-Xmx)
NewSize = 88604672 (84.5MB) // 新生代初始大小 (-Xmn 或通过 NewRatio 计算)
MaxNewSize = 1418199040 (1352.5MB) // 新生代最大大小
OldSize = 177733632 (169.5MB) // 老年代大小
NewRatio = 2 // 新生代与老年代比例 (老年代/新生代)
SurvivorRatio = 8 // Eden 区与 Survivor 区比例 (Eden/Survivor)
MetaspaceSize = 21807104 (20.796875MB) // 元空间初始大小
CompressedClassSpaceSize = 1073741824 (1024.0MB) // 压缩类空间大小
MaxMetaspaceSize = 17592186044415 MB // 元空间最大大小 (非常大,接近无限)
G1HeapRegionSize = 0 (0.0MB) // G1 GC 的 Region 大小 (未使用 G1 时为 0)
// ---- 堆使用情况 (Heap Usage) ----
Heap Usage:
PS Young Generation // Parallel Scavenge 管理的新生代
Eden Space: // Eden 区
capacity = 66584576 (63.5MB) // 当前容量
used = 31136256 (29.69384765625MB) // 已使用
free = 35448320 (33.80615234375MB) // 剩余空间
46.761964812992126% used // 使用率
From Space: // Survivor From 区
capacity = 11010048 (10.5MB)
used = 0 (0.0MB)
free = 11010048 (10.5MB)
0.0% used
To Space: // Survivor To 区
capacity = 11010048 (10.5MB)
used = 0 (0.0MB)
free = 11010048 (10.5MB)
0.0% used
PS Old Generation // Parallel Old 管理的老年代
capacity = 177733632 (169.5MB)
used = 0 (0.0MB)
free = 177733632 (169.5MB)
0.0% used
// ---- 字符串常量池信息 ----
3163 interned Strings occupying 259640 bytes. // 字符串常量池中有 3163 个字符串,占用约 259KB
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
b.txt
(-histo
输出) 内容解读:
num #instances #bytes class name // 表头:序号、实例数量、总字节数、类名
----------------------------------------------
1: 1610 44293784 [B // 排名第一的是 byte[] 数组,有 1610 个实例,总共占用约 44MB
2: 659 2906760 [I // int[] 数组
3: 7774 906192 [C // char[] 数组 (通常由 String 内部持有)
4: 5933 142392 java.lang.String // String 对象本身 (不含内部 char[])
5: 691 79112 java.lang.Class // Class 对象
6: 1305 67680 [Ljava.lang.Object; // Object[] 数组
7: 791 31640 java.util.TreeMap$Entry
8: 628 25120 java.util.LinkedHashMap$Entry
9: 456 22168 [Ljava.lang.String; // String[] 数组
10: 364 11648 java.util.HashMap$Node
... (省略后续内容) ...
Total 24100 48649392 // 汇总:总共有 24100 个对象实例,总共占用约 48MB
2
3
4
5
6
7
8
9
10
11
12
13
14
[B
表示byte[]
数组。[I
表示int[]
数组。[C
表示char[]
数组。[L<classname>;
表示该类的对象数组,例如[Ljava.lang.String;
就是String[]
。
-histo
的输出对于快速定位哪个类的实例最多或哪个类的实例总占用空间最大非常有用,是排查内存问题的常用手段。如果加上 :live
,则只统计 GC 后仍然存活的对象。
jmap
的注意事项(重要)
- Stop-The-World (STW):
jmap
(尤其是-dump
和-histo
)需要暂停目标 JVM 的所有应用线程(进入安全点 SafePoint)来保证内存数据的一致性。在生产环境中对一个繁忙的应用执行jmap
可能会导致较长时间的卡顿,需要谨慎使用。 - 安全点偏差 (Safepoint Bias): 由于
jmap
获取的是安全点时刻的快照,那些生命周期恰好在两个安全点之间的对象可能不会被统计到(尤其是在使用:live
选项时),这可能导致分析结果存在一定的偏差。 - 等待安全点: 如果某个线程长时间无法到达安全点(例如执行 JNI 本地代码或处于长时间循环中),
jmap
命令可能会一直阻塞等待。
相比之下,jstat
通常对应用影响较小,因为它读取的是 JVM 主动维护的性能计数器,不需要长时间暂停应用。
# 6. jhat:交互式堆转储快照分析工具 (已移除)
jhat
(JVM Heap Analysis Tool) 是早期 JDK 提供的一个与 jmap
配套使用的工具,用于分析 jmap
生成的堆转储快照 (.hprof
文件)。
jhat
的主要特点是它会解析 dump 文件,并在本地启动一个微型的 HTTP/HTML 服务器(默认端口 7000)。用户可以通过浏览器访问该服务器,交互式地查看分析结果,例如查看类信息、对象实例、引用关系,甚至执行对象查询语言 (OQL)。
重要说明:jhat
命令在 JDK 9 及之后的版本中已经被移除。官方推荐使用更强大的内存分析工具,如 Eclipse Memory Analyzer (MAT) 或 VisualVM 来替代。jhat
在分析大型堆 dump 文件时性能较差,且功能相对有限。
基本语法 (仅适用于 JDK 8 及之前版本):
jhat [options] <dumpfile>
<dumpfile>
:jmap
生成的.hprof
文件路径。
常用 options
参数:
option 参数 | 作用 |
---|---|
-stack false|true | 关闭/打开对象分配调用栈跟踪 (分析需要 dump 文件包含栈信息) |
-refs false|true | 关闭/打开对象引用跟踪 |
-port <port-number> | 设置 jhat HTTP 服务器的端口号,默认为 7000。 |
-exclude <file> | 指定一个文件,列出在对象查询时需要排除的数据成员(属性)。 |
-baseline <file> | 指定一个基准堆转储文件,用于对比分析两个 dump 文件。 |
-J<flag> | 向运行 jhat 的 JVM 传递参数,例如 -J-Xmx1024m (处理大 dump 时可能需要调大内存)。 |
示例 (假设使用 JDK 8):
# 分析 D 盘的 3.hprof 文件
jhat d:\3.hprof
2
命令行会显示启动信息,并提示服务器正在监听 7000 端口:
图:jhat 启动信息
然后,在浏览器中访问 http://localhost:7000/
:
图:jhat 的 Web 分析界面
界面提供了多种链接来查看堆信息:
Show heap histogram
: 显示与jmap -histo
类似的对象统计直方图。Show finalizer summary
: 查看等待finalize
的对象。Execute Object Query Language (OQL) query
: 执行 OQL 查询。
OQL 查询示例:查找所有长度大于 100 的 String
对象。
select s from java.lang.String s where s.value.length > 100
# 7. jstack:捕获 JVM 线程快照 (Thread Dump)
官方文档参考 (Java 11):
https://docs.oracle.com/en/java/javase/11/tools/jstack.html
jstack
(JVM Stack Trace) 是用于生成目标 Java 进程在某一时刻的线程快照 (Thread Dump) 的命令。线程快照包含了该进程内每一条线程当前正在执行的方法堆栈信息的集合。
核心用途:
- 定位线程长时间停顿的原因:例如:
- 线程间死锁 (Deadlock):多个线程互相等待对方持有的锁。
- 死循环 (Infinite Loop):线程在代码中无限循环,消耗 CPU。
- 等待外部资源:线程等待网络 I/O、数据库响应、磁盘读写等,导致长时间阻塞或等待。
- 等待锁 (Monitor Entry / Wait):线程等待获取某个对象的监视器锁,或调用了
Object.wait()
/Condition.await()
等待通知。
- 分析 CPU 占用过高:结合操作系统的
top
/prstat
等命令找到高 CPU 占用的 Java 线程(得到其原生线程 ID),再用jstack
找到对应的 Java 线程堆栈,分析其正在执行的代码。
线程状态关注点:
在分析 jstack
输出的线程 dump 时,需要特别关注以下几种线程状态:
Deadlock
: 死锁状态,jstack
会明确指出死锁涉及的线程和锁对象,是最需要优先处理的问题。WAITING (on object monitor)
/TIMED_WAITING (on object monitor)
: 线程调用了Object.wait()
或LockSupport.park()
等待被唤醒。需要分析为何长时间未被唤醒。WAITING (parking)
/TIMED_WAITING (parking)
: 通常是调用了LockSupport.park()
系列方法,常见于 JUC 包中的锁和同步器。BLOCKED (on object monitor)
: 线程正在等待进入synchronized
代码块/方法,因为锁被其他线程持有。大量线程处于BLOCKED
状态通常意味着激烈的锁竞争。RUNNABLE
: 线程处于可运行状态,可能正在执行代码,也可能在等待操作系统分配 CPU 时间片。如果一个线程长时间处于RUNNABLE
状态且 CPU 占用高,可能是在执行计算密集型任务或死循环。TIMED_WAITING (sleeping)
: 线程调用了Thread.sleep()
。NEW
: 线程已创建但尚未启动。TERMINATED
: 线程已执行完毕。
基本语法:
jstack [option] <pid>
option
参数详解:
option 参数 | 作用 |
---|---|
-F | 强制模式。当正常的 jstack 请求没有响应时(例如进程挂起),强制输出线程堆栈。 |
-l | 显示关于锁的附加信息。除了堆栈,还会打印出 locked <addr> (a <classname>) 和 waiting to lock <addr> (a <classname>) 等锁信息,对于分析锁竞争和死锁非常有帮助。强烈推荐使用! |
-m | 混合模式。如果线程调用了 JNI (Java Native Interface) 本地方法,同时打印 Java 堆栈和 C/C++ 堆栈信息(需要操作系统支持)。 |
代码示例:模拟死锁 (ThreadDeadLock.java
)
import java.util.Map;
import java.util.Set;
/**
* 演示线程死锁的示例代码。
* 两个线程互相持有对方需要的锁,导致死锁。
*/
public class ThreadDeadLock {
public static void main(String[] args) {
final StringBuilder s1 = new StringBuilder(); // 锁对象1
final StringBuilder s2 = new StringBuilder(); // 锁对象2
// 线程 1
new Thread("T1-持有s1等待s2") { // 给线程命名,方便 jstack 分析
@Override
public void run() {
synchronized (s1) { // 获取 s1 的锁
s1.append("a");
s2.append("1");
System.out.println(Thread.currentThread().getName() + " 获取 s1,尝试获取 s2...");
try {
// 短暂休眠,增加死锁发生的概率
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 尝试获取 s2 的锁(此时 s2 可能被线程 2 持有)
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
// 线程 2
new Thread("T2-持有s2等待s1") { // 给线程命名
@Override
public void run() {
synchronized (s2) { // 获取 s2 的锁
s1.append("c");
s2.append("3");
System.out.println(Thread.currentThread().getName() + " 获取 s2,尝试获取 s1...");
try {
// 短暂休眠
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 尝试获取 s1 的锁(此时 s1 可能被线程 1 持有)
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
// 主线程休眠一会儿,等待死锁发生
try {
Thread.sleep(1000);
System.out.println("死锁应该已经发生,准备执行 jstack...");
} catch (InterruptedException e) {
e.printStackTrace();
}
// (可选) 在程序内部打印所有线程堆栈信息,效果类似 jstack
/*
new Thread(() -> {
Map<Thread, StackTraceElement[]> all = Thread.getAllStackTraces();
Set<Map.Entry<Thread, StackTraceElement[]>> entries = all.entrySet();
for(Map.Entry<Thread, StackTraceElement[]> en : entries){
Thread t = en.getKey();
StackTraceElement[] v = en.getValue();
System.out.println("\n【Thread name is :" + t.getName() + " ID:" + t.getId() + " State:" + t.getState() + "】");
for(StackTraceElement s : v){
System.out.println("\t at " + s.toString());
}
}
}).start();
*/
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
执行 jstack
命令:
运行 ThreadDeadLock
程序,它会很快进入死锁状态。然后通过 jps
找到该进程的 PID (假设为 22164
),执行 jstack
(推荐带 -l
参数):
# 使用 jps 找到 PID
jps
# 执行 jstack,并附带 -l 参数显示锁信息
jstack -l 22164
2
3
4
5
jstack -l
输出解读 (关键部分):
2022-01-31 21:51:08
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.231-b11 mixed mode):
// ... (其他线程信息,如 VM Thread, GC threads, Compiler threads, Finalizer, Reference Handler 等)
// --- 线程 2 (T2) ---
"T2-持有s2等待s1" #13 prio=5 os_prio=0 tid=0x000000001e041800 nid=0x9bc waiting for monitor entry [0x000000001fd1f000]
java.lang.Thread.State: BLOCKED (on object monitor) // 状态:阻塞,等待获取锁
at com.youngkbt.jstack.ThreadDeadLock$2.run(ThreadDeadLock.java:63) // 阻塞发生在第 63 行 (尝试获取 s1)
- waiting to lock <0x000000076ba1e2f0> (a java.lang.StringBuilder) // 正在等待获取地址为 ...e2f0 的 StringBuilder (s1) 的锁
- locked <0x000000076ba1e338> (a java.lang.StringBuilder) // 当前持有地址为 ...e338 的 StringBuilder (s2) 的锁
// --- 线程 1 (T1) ---
"T1-持有s1等待s2" #12 prio=5 os_prio=0 tid=0x000000001e03b800 nid=0x52f8 waiting for monitor entry [0x000000001fc1f000]
java.lang.Thread.State: BLOCKED (on object monitor) // 状态:阻塞,等待获取锁
at com.youngkbt.jstack.ThreadDeadLock$1.run(ThreadDeadLock.java:35) // 阻塞发生在第 35 行 (尝试获取 s2)
- waiting to lock <0x000000076ba1e338> (a java.lang.StringBuilder) // 正在等待获取地址为 ...e338 的 StringBuilder (s2) 的锁
- locked <0x000000076ba1e2f0> (a java.lang.StringBuilder) // 当前持有地址为 ...e2f0 的 StringBuilder (s1) 的锁
// ... (其他线程信息)
// --- 死锁检测部分 ---
Found one Java-level deadlock:
=============================
"T2-持有s2等待s1":
waiting to lock monitor 0x000000001e044d58 (object 0x000000076ba1e2f0, a java.lang.StringBuilder), // T2 等待锁 ...e2f0 (s1)
which is held by "T1-持有s1等待s2" // 该锁被 T1 持有
"T1-持有s1等待s2":
waiting to lock monitor 0x000000001c7c2dc8 (object 0x000000076ba1e338, a java.lang.StringBuilder), // T1 等待锁 ...e338 (s2)
which is held by "T2-持有s2等待s1" // 该锁被 T2 持有
Java stack information for the threads listed above:
===================================================
"T2-持有s2等待s1":
at com.youngkbt.jstack.ThreadDeadLock$2.run(ThreadDeadLock.java:63)
- waiting to lock <0x000000076ba1e2f0> (a java.lang.StringBuilder)
- locked <0x000000076ba1e338> (a java.lang.StringBuilder)
"T1-持有s1等待s2":
at com.youngkbt.jstack.ThreadDeadLock$1.run(ThreadDeadLock.java:35)
- waiting to lock <0x000000076ba1e338> (a java.lang.StringBuilder)
- locked <0x000000076ba1e2f0> (a java.lang.StringBuilder)
Found 1 deadlock. // 明确提示发现了 1 个死锁
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
解读:
jstack
清晰地列出了 T1 和 T2 线程都处于BLOCKED
状态。- 通过
-l
参数提供的锁信息,我们可以看到 T1 持有s1
(...e2f0
) 的锁,并等待s2
(...e338
) 的锁;而 T2 持有s2
(...e338
) 的锁,并等待s1
(...e2f0
) 的锁。 jstack
在输出的末尾直接检测并报告了 Java 级别的死锁,明确指出了涉及的线程和它们互相等待的锁对象。这极大地简化了死锁问题的定位。
其他示例代码 (用于练习 jstack
):
线程睡眠 (
TreadSleepTest.java
):/** * 演示线程长时间 sleep 的情况。 * jstack 会显示 main 线程处于 TIMED_WAITING (sleeping) 状态。 */ public class TreadSleepTest { public static void main(String[] args) { System.out.println("hello - 1"); try { // 让主线程睡眠 10 分钟 Thread.sleep(1000 * 60 * 10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("hello - 2"); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16线程同步 (
ThreadSyncTest.java
):/** * 演示多个线程竞争同一个锁 (synchronized)。 * jstack 会显示一个线程持有锁处于 RUNNABLE 或 TIMED_WAITING (sleeping) 状态, * 而其他线程则处于 BLOCKED (on object monitor) 状态,等待获取锁。 */ public class ThreadSyncTest { public static void main(String[] args) { Number number = new Number(); Thread t1 = new Thread(number, "线程1"); // 命名线程 Thread t2 = new Thread(number, "线程2"); // 命名线程 t1.start(); t2.start(); } } class Number implements Runnable { private int number = 1; private final Object lock = new Object(); // 或者直接 synchronized(this) @Override public void run() { while (true) { synchronized (lock) { // 同步块,保证 number 操作的原子性 if (number <= 100) { try { // 模拟耗时操作 Thread.sleep(10); // 减少 sleep 时间以增加竞争 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + number); number++; } else { break; // 结束循环 } } // 锁在这里释放 try { // 在锁外 sleep,减少持有锁的时间,但仍可能观察到 BLOCKED Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 8. jcmd:多功能 JVM 诊断命令
官方文档参考 (Java 11):
https://docs.oracle.com/en/java/javase/11/tools/jcmd.html
jcmd
(Java Command) 是 JDK 1.7 及以后版本引入的一个多功能命令行工具。它的目标是整合其他多个命令的功能(除了 jstat
的实时统计流),提供一个统一的接口来与 JVM 进行交互和诊断。Oracle 官方也推荐使用 jcmd
来替代 jmap
的部分功能。
核心优势:
- 功能整合:可以用
jcmd
执行许多原本需要jps
,jinfo
,jmap
,jstack
才能完成的操作。 - 无需 JMX:与
jinfo
,jmap
等不同,jcmd
发送命令给目标 JVM 进程时,不需要 JMX Agent 的支持,减少了配置的复杂性。 - 动态加载:支持向运行中的 JVM 请求执行特定的诊断命令。
常用命令格式:
列出所有 Java 进程 (
jcmd -l
):功能类似于jps
。jcmd -l
1图:jcmd -l 输出示例
查看指定进程支持的命令 (
jcmd <pid> help
):列出目标 JVM 实例当前可用的所有jcmd
命令。jcmd 10561 help
1图:jcmd help 输出示例
执行具体诊断命令 (
jcmd <pid> <command> [arguments]
):向目标进程发送并执行具体的诊断命令。
常用 jcmd
命令及其替代功能:
jcmd <pid> Thread.print
: 替代jstack <pid>
,打印线程堆栈。可以加上-l
参数获取锁信息 (jcmd <pid> Thread.print -l
)。jcmd <pid> GC.class_histogram
: 替代jmap -histo <pid>
,打印堆对象直方图。jcmd <pid> GC.heap_dump <filename>
: 替代jmap -dump:format=b,file=<filename> <pid>
,生成堆转储快照。jcmd <pid> GC.run
: 手动触发一次 Full GC (谨慎在生产环境使用)。jcmd <pid> GC.run_finalization
: 手动触发System.runFinalization()
。jcmd <pid> VM.uptime
: 查看 JVM 的启动时长,类似jstat -t
的第一列。jcmd <pid> VM.system_properties
: 替代jinfo -sysprops <pid>
,查看 Java 系统属性。jcmd <pid> VM.flags [-all]
: 替代jinfo -flags <pid>
,查看 JVM 参数。-all
显示所有参数(包括默认值)。jcmd <pid> VM.command_line
: 查看 JVM 启动的完整命令行参数。jcmd <pid> VM.version
: 查看 JVM 版本信息。
jcmd
示例:
替代
jmap -histo
:# 打印进程 10561 的对象直方图 jcmd 10561 GC.class_histogram
1
2输出结果与
jmap -histo
类似。替代
jmap -dump
:# 将进程 10561 的堆 dump 到 D 盘的 m.hprof 文件 jcmd 10561 GC.heap_dump d:\m.hprof
1
2执行后会在 D 盘生成
m.hprof
文件。
# 9. jstatd:开启远程 JVM 监控
jstatd
(Java Statistics Monitoring Daemon) 是一个 RMI (Remote Method Invocation) 服务端程序。它的主要作用是允许远程监控工具(如 jps
, jstat
, VisualVM 等)连接到本机,并获取本机上运行的 Java 应用程序的性能数据。
简单来说,jstatd
扮演了一个代理服务器的角色,负责收集本机 JVM 的信息,并通过 RMI 协议将其暴露给远程连接的客户端。
图:jstatd 工作原理示意图
启动 jstatd
:
启动 jstatd
通常需要指定安全策略文件,以控制哪些远程主机可以连接以及允许执行哪些操作。一个简单的(但不安全的)策略文件 jstatd.all.policy
可能如下:
grant codebase "file:${java.home}/../lib/tools.jar" {
permission java.security.AllPermission;
};
2
3
启动命令示例:
# -J-Djava.security.policy 指定策略文件
# -p 指定 RMI 端口 (可选,默认 1099)
jstatd -J-Djava.security.policy=jstatd.all.policy
2
3
使用远程工具连接:
启动 jstatd
后,就可以在另一台机器上使用 jps
或 jstat
等工具,通过 hostid
参数连接到运行 jstatd
的机器了。
# 查看远程主机 a.b.c.d 上的 Java 进程
jps a.b.c.d
# 监控远程主机 a.b.c.d 上 PID 为 1234 的进程的 GC 情况
jstat -gc 1234@a.b.c.d 1s
2
3
4
5
安全警告:jstatd
暴露了本地 JVM 的大量信息,直接在生产环境或不受信任的网络中运行 jstatd
(尤其是使用 AllPermission
策略)存在严重的安全风险。建议优先考虑使用 SSH 隧道或其他更安全的监控方案(如 JMX + 认证/SSL,或 APM 系统)。如果必须使用 jstatd
,务必配置严格的安全策略。