虚拟机性能监控、故障处理工具
1. 基础故障处理工具
Java 开发人员肯定都知道 JDK 的 bin 目录中有 java.exe、javac.exe 这两个命令行工具,但并非所有程序员都了解过 JDK 的 bin 目录下其他各种小工具的作用。随着 JDK 版本的更迭,这些小工具的数量和功能也在不知不觉地增加与增强。除了编译和运行 Java 程序外,打包、部署、签名、调试、监控、运维等各种场景都可能会用到它们,这些工具如下图所示。
在本章,笔者将介绍这些工具中的一部分,主要是用于监视虚拟机运行状态和进行故障处理的工具。这些故障处理工具并不单纯是被 Oracle 公司作为“礼物”附赠给JDK的使用者,根据软件可用性和授权的不同,可以把它们划分成三类:
- 商业授权工具:主要是 JMC(Java Mission Control)及它要使用到的 JFR(Java Flight Recorder),JMC 这个原本来自于 JRockit 的运维监控套件从 JDK 7 Update 40 开始就被集成到 OracleJDK 中,JDK 11 之前都无须独立下载,但是在商业环境中使用它则是要付费的。
- 正式支持工具:这一类工具属于被长期支持的工具,不同平台、不同版本的 JDK 之间,这类工具可能会略有差异,但是不会出现某一个工具突然消失的情况。
- 实验性工具:这一类工具在它们的使用说明中被声明为“没有技术支持,并且是实验性质的”(Unsupported and Experimental)产品,日后可能会转正,也可能会在某个JDK版本中无声无息地消失。但事实上它们通常都非常稳定而且功能强大,也能在处理应用程序性能问题、定位故障时发挥很大的作用。
读者如果比较细心的话,还可能会注意到这些工具程序大多数体积都异常小。并非 JDK 开发团队刻意把它们制作得如此精炼、统一,而是因为这些命令行工具大多仅是一层薄包装而已,真正的功能代码是实现在 JDK 的工具类库中的。
本章所讲解的工具大多基于 Windows 平台下的 JDK 进行演示。
1.1 jps:虚拟机进程状况工具
jps(JVM Process Status Tool):可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main() 函数所在的类)名称以及这些进程的本地虚拟机唯一 ID(LVMID,Local Virtual Machine Identifier)。
虽然功能比较单一,但它绝对是使用频率最高的 JDK 命令行工具,因为其他的 JDK 工具大多需要输入它查询到的 LVMID 来确定要监控的是哪一个虚拟机进程。对于本地虚拟机进程来说,LVMID 与操作系统的进程ID(PID,Process Identifier)是一致的,使用 Windows 的任务管理器或者 UNIX 的 ps 命令也可以查询到虚拟机进程的 LVMID,但如果同时启动了多个虚拟机进程,无法根据进程名称定位时,那就必须依赖 jps 命令显示主类的功能才能区分了。
jps 命令格式:
C:\Users\罗志宏>jps -help
usage: jps [-help]
jps [-q] [-mlvV] [<hostid>]
Definitions:
<hostid>: <hostname>[:<port>]
jps 的其他常用选项见下表:
选项 | 作用 |
---|---|
-q | 只输出 LVMID,省略主类的名称 |
-m | 输出虚拟机进程启动时传递给主类 main() 函数的参数 |
-l | 输出主类的全名,如果进程执行的是 JAR 包,则输出 JAR 路径 |
-v | 输出虚拟机进程启动时的 JVM 参数 |
执行示例:
C:\Users\罗志宏>jps -l
29376 org.jetbrains.jps.cmdline.Launcher
31892 sun.tools.jps.Jps
3620
18344 org.jetbrains.jps.cmdline.Launcher
28312 com.ApplicationContext
jps 还可以通过 RMI 协议查询开启了 RMI 服务的远程虚拟机进程状态,参数 hostid 为 RMI 注册表中注册的主机名
1.2 jstat:虚拟机统计信息监视工具
jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据,在没有 GUI 图形界面、只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的常用工具。
jps 命令格式:
C:\Users\罗志宏>jstat -help
Usage: jstat -help|-options
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
Definitions:
<option> An option reported by the -options option
<vmid> 虚拟机的标识符. 一个 vmid 采用如下格式:
<lvmid>[@<hostname>[:<port>]]
如果是本地虚拟机进程,VMID 与 LVMID 是一致的; <hostname> 是运行
目标Java虚拟机的主机名;<port> 是rmiregistry 上注册的目标虚拟机的端口号.
请参阅jvmstat文档获取更完整的虚拟机标识符的描述内容.
<lines> 标题行之间的样本数。
<interval> 采样间隔。允许使用下列表格: <n>[“ms”|“s”)
<n>是整数和后缀指定单位为
毫秒(“ms”)或秒(“s”)。默认单位为“ms”。
<count> 终止前取样的数量。
-J<flag> Pass <flag> directly to the runtime system.
选项 option 代表用户希望查询的虚拟机信息,主要分为三类:类加载、垃圾收集、运行期编译状况。详细请参考下表的描述。
选项 | 作用 |
---|---|
-class | 监视类加载、卸载数量、总空间以及类装载所耗费的时间 |
-gc | 监视 java 堆 gc 情况 |
-gccapacity | 监视内容与 -gc 基本相同,但输出主要关注 java 堆各个区域使用到的最大、最小空间 |
-gcutil | 监视内容与 -gc 基本相同,但输出主要关注已使用空间占总空姐的百分比 |
-gccause | 与 -gcutil 功能一样,但是会额外输出导致上一次垃圾收集产生的原因 |
-gcnew | 监视新生代垃圾收集状况 |
-gcnewcapacity | 监视内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间 |
-gcold | 监视老年代垃圾收集情况 |
-gcoldcapacity | 监视内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间 |
-gcpermcapacity | 输出永久代使用到的最大、最小空间 |
-compiler | 输出即使编译器编译过的方法、耗时等信息 |
-printcompilation | 输出已经被即使编译的方法 |
执行示例:
C:\Users\罗志宏>jstat -gcutil 28312
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 100.00 73.96 8.18 93.94 88.93 11 0.194 0 0.000 0.194
使用 jstat 工具在纯文本状态下监视虚拟机状态的变化,在用户体验上也许不如后文将会提到的 JMC、VisualVM 等可视化的监视工具直接以图表展现那样直观,但在实际生产环境中不一定可以使用图形界面,而且多数服务器管理员也都已经习惯了在文本控制台工作,直接在控制台中使用jstat命令依然是一种常用的监控方式。
1.3 jinfo:Java配置信息工具
jinfo(Configuration Info for Java)的作用是实时查看和调整虚拟机各项参数。
使用 jps 命令的 -v 参数可以查看虚拟机启动时显式指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,除了去找资料外,就只能使用 jinfo 的 -flag 选项进行查询了(如果只限于JDK 6 或以上版本的话,使用java-XX:+PrintFlagsFinal查看参数默认值也是一个很好的选择)。
jinfo 命令格式:
C:\WINDOWS\system32>jinfo -help
Usage:
jinfo [option] <pid>
(to connect to running process)
jinfo [option] <executable <core>
(to connect to a core file)
jinfo [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server)
where <option> is one of:
-flag <name> 打印虚拟机 flag 的值
-flag [+|-]<name> 开启或关闭命名虚拟机 flag
-flag <name>=<value> 给虚拟机 flag 设置给定的值
-flags 打印虚拟机 flags
-sysprops 打印Java system properties
<no option> to print both of the above
-h | -help to print this help message
执行实例:
C:\WINDOWS\system32>jinfo -flag UseG1GC 28312
-XX:+UseG1GC // 开启了G1收集器
C:\WINDOWS\system32>jinfo -flag MaxHeapSize 28312
-XX:MaxHeapSize=524288000 // 最大堆内存500m
// 几个特殊的XX参数
-Xmx :jvm的最大值 -XX:MaxHeapSize 的简写
-Xms :jvm的最小值 -XX:InitialHeapSize 的简写
-Xss -XX:ThreadStackSize 的简写 Stack 栈
JDK 6 之后,jinfo 在 Windows 和 Linux 平台都有提供,并且加入了在运行期修改部分参数值的能力(可以使用 -flag[+|-]name 或者 -flag name=value 在运行期修改一部分运行期可写的虚拟机参数值)。在 JDK 6 中,jinfo 对于 Windows 平台功能仍然有较大限制,只提供了最基本的 -flag 选项。
1.4 jmap:Java内存映像工具
jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。
如果不使用 jmap 命令,要想获取 Java 堆转储快照也还有一些比较“暴力”的手段:-XX:+HeapDumpOnOutOfMemoryError 参数,可以让虚拟机在内存溢出异常出现之后自动生成堆转储快照文件;通过 -XX:+HeapDumpOnCtrlBreak 参数则可以使用 [Ctrl]+[Break] 键让虚拟机生成堆转储快照文件;又或者在Linux 系统下通过 Kill-3 命令发送进程退出信号“恐吓”一下虚拟机,也能顺利拿到堆转储快照。
jmap 的作用并不仅仅是为了获取堆转储快照,它还可以查询 finalize 执行队列、Java 堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。
和 jinfo 命令一样,jmap 有部分功能在 Windows 平台下是受限的,除了生成堆转储快照的 -dump 选项
和用于查看每个类的实例、空间占用统计的 -histo 选项在所有操作系统中都可以使用之外,其余选项都
只能在 Linux/Solaris 中使用。
jmap 命令格式:
C:\WINDOWS\system32>jmap -help
Usage:
jmap [option] <pid>
(to connect to running process)
jmap [option] <executable <core>
(to connect to a core file)
jmap [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server)
option 选项的合法值与具体含义如下表所示
选项 | 作用 |
---|---|
-dump | 生成 java 堆转储快照。格式为 -dump:[live],format=b,file=<filename>,其中 live 子参数说明是否只 dump 出存活的对象 |
-finalizerinfo | 显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象。只在 Linux/Solaris 平台下有效 |
-heap | 显示 java 堆详细信息,如使用那种回收器、参数配置、分代状态等。只在 Linux/Solaris 平台下有效 |
-histo | 显示堆中对象统计信息,包括类、实例数量、合计容量 |
-permstat | 以 ClassLoader 为统计口径显示永久代内存状态。只在 Linux/Solaris 平台下有效 |
-F | 当虚拟机进程堆 -dump 选项没有响应时,可使用这个选项强制生成 dump 快照。只在 Linux/Solaris 平台下有效 |
执行实例:使用 jmap 生成一个正在运行的 dump 堆转储快照文件。例子中的 28312 是通过 jps 命令查询到的 LVMID。
C:\WINDOWS\system32>jmap -dump:format=b,file=dump.bin 28312
Dumping heap to C:\Windows\System32\dump.bin ...
Heap dump file created
1.5 jhat:虚拟机堆转储快照分析工具
JDK 提供 jhat(JVM Heap Analysis Tool)命令与 jmap 搭配使用,来分析 jmap 生成的堆转储快照。
jhat 内置了一个微型的 HTTP/Web 服务器,生成堆转储快照的分析结果后,可以在浏览器中查看。不过实事求是地说,在实际工作中,除非手上真的没有别的工具可用,否则多数人是不会直接使用 jhat 命令来分析堆转储快照文件的,主要原因有两个方面。一是一般不会在部署应用程序的服务器上直接分析堆转储快照,即使可以这样做,也会尽量将堆转储快照文件复制到其他机器上进行分析,因为分析工作是一个耗时而且极为耗费硬件资源的过程,既然都要在其他机器上进行,就没有必要再受命令行工具的限制了。另外一个原因是 jhat 的分析功能相对来说比较简陋,后文将会介绍到的 VisualVM,以及专业用于分析堆转储快照文件的 Eclipse Memory Analyzer、IBM HeapAnalyzer 等工具,都能实现比 jhat 更强大专业的分析功能。
执行示例:使用jhat 分析 dump 文件
C:\WINDOWS\system32>jhat C:\Windows\System32\dump.bin
Reading from C:\Windows\System32\dump.bin...
Dump file created Sun May 02 20:28:12 CST 2021
......
................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
用户在浏览器中输入http://localhost:7000/可以看到分析结果
分析结果默认以包为单位进行分组显示,分析内存泄漏问题主要会使用到其中的 “Heap Histogram”(与jmap-histo功能一样)与 OQL 页签的功能,前者可以找到内存中总容量最大的对象,后者是标准的对象查询语言,使用类似 SQL 的语法对内存中的对象进行查询统计。
1.6 jstack:Java堆栈跟踪工具
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为 threaddump 或者 javacore文件)。
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。线程出现停顿时通过 jstack 来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
jstack 命令格式:
C:\WINDOWS\system32>jstack -help
Usage:
jstack [ option ] <pid>
......
option 选项的合法值与具体含义如下表
选项 | 作用 |
---|---|
-F | 当正常输出的请求不被响应时,强制输出线程堆栈 |
-l | 除堆栈外,显示关于锁的附加信息 |
-m | 如果调用到本地方法的话,可以显示 C/C++ 的堆栈 |
有如下程序:
public class CpuTest {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(){
@Override
public void run() {
while (true) {
int a = 1 / 2;
}
}
}.start();
}
}
}
使用示例:使用 jstack 查看 CpuTest 线程堆栈。22400 是通过 jps 命令查询到的 LVMID。
C:\WINDOWS\system32>jstack -l 22400
2021-05-02 21:57:48
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.211-b12 mixed mode):
"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x0000000002df3000 nid=0x2114 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Thread-1" #13 prio=5 os_prio=0 tid=0x000000001e807000 nid=0x3148 runnable [0x000000001f72e000]
java.lang.Thread.State: RUNNABLE
at CpuTest$1.run(CpuTest.java:12)
Locked ownable synchronizers:
- None
......
从 JDK 5 起,java.lang.Thread 类新增了一个 getAllStackTraces() 方法用于获取虚拟机中所有线程的StackTraceElement 对象。使用这个方法可以通过简单的几行代码完成 jstack 的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈。
查看线程状况的 JSP 页面,需要的时候执行下就可以
<%@ page import="java.util.Map"%>
<html>
<head>
<title>服务器线程信息</title>
</head>
<body>
<pre>
<%
for (Map.Entry<Thread, StackTraceElement[]> stackTrace : Thread.getAllStack-Traces().entrySet()) {
Thread thread = (Thread) stackTrace.getKey();
StackTraceElement[] stack = (StackTraceElement[]) stackTrace.getValue();
if (thread.equals(Thread.currentThread())) {
continue;
}
out.print("\n线程:" + thread.getName() + "\n");
for (StackTraceElement element : stack) {
out.print("\t"+element+"\n");
}
}
%>
</pre>
</body>
</html>
2. 可视化故障处理工具
JDK 中除了附带大量的命令行工具外,还提供了几个功能集成度更高的可视化工具,用户可以使用这些可视化工具以更加便捷的方式进行进程故障诊断和调试工作。这类工具主要包括 JConsole、JHSDB、VisualVM 和 JMC 四个。
2.1 JHSDB:基于服务性代理的调试工具
JDK 中提供了 JCMD 和 JHSDB 两个集成式的多功能工具箱,它们不仅整合了上文介绍到的所有基础工具所能提供的专项功能,而且由于有着“后发优势”,能够做得往往比之前的老工具们更好、更强大。JHSDB 是 JDK 9 中才正式提供。
JCMD、JHSDB和基础工具的对比
本节的主题是可视化的故障处理,所以 JCMD 及 JHSDB 的命令行模式就不再作重点讲解了,读者可参考上文的基础命令,再借助它们在 JCMD 和 JHSDB 中的 help 去使用,相信是很容易举一反三、触类旁通的。接下来笔者要通过一个实验来讲解 JHSDB 的图形模式下的功能。
// TODO
2.2 JConsole:Java监视与管理控制台
JConsole(Java Monitoring and Management Console)是一款基于JMX(Java Manage-ment Extensions)的可视化监视、管理工具。
它的主要功能是通过J MX 的 MBean(Managed Bean)对系统进行信息收集和参数动态调整。JMX 是一种开放性的技术,不仅可以用在虚拟机本身的管理上,还可以运行于虚拟机之上的软件中,典型的如中间件大多也基于 JMX 来实现管理与监控。虚拟机对 JMX MBean 的访问也是完全开放的,可以使用代码调用 API、支持 JMX 协议的管理控制台,或者其他符合 JMX 规范的软件进行访问。
2.2.1 启动JConsole
通过 JDK/bin 目录下的 jconsole.exe 启动 JCon-sole 后,会自动搜索出本机运行的所有虚拟机进程,而不需要用户自己使用 jps 来查询,如下图所示。双击选择其中一个进程便可进入主界面开始监控。JMX 支持跨服务器的管理,也可以使用下面的“远程进程”功能来连接远程服务器,对远程虚拟机进行监控。
这里 MonitoringTest 是笔者准备的“反面教材”代码之一。双击它进入 JConsole 主界面,可以看到主界面里共包括概述、内存、线程、类、VM摘要、MBean 六个页签。
这里注意如果不安全连接失败,需要加入如下 JVM 启动参数:
-Dcom.sun.management.jmxremote // 是否支持远程JMX访问,默认true
-Dcom.sun.management.jmxremote.port=8011 // 监听端口号,方便远程访问
-Dcom.sun.management.jmxremote.ssl=false // 是否需要开启用户认证,默认开启
-Dcom.sun.management.jmxremote.authenticate=false // 是否对连接开启SSL加密,默认开启
2.2.2 内存监控
内存页签的作用相当于可视化的 jstat 命令,用于监视被收集器管理的虚拟机内存(被收集器直接管理的 Java 堆和被间接管理的方法区)的变化趋势。我们通过运行如下代码来体验一下它的监视功能。运行时设置的虚拟机参数为:
-Xms100m
-Xmx100m
-XX:+UseSerialGC
JConsole 监视代码
public class MonitoringTest {
/**
* 内存占位符对象,一个OOMObject大约占64KB
*/
static class OOMObject {
public byte[] placeholder = new byte[64 * 1024];
}
public static void fillHeap(int num) throws InterruptedException {
List<OOMObject> list = new ArrayList<OOMObject>();
for (int i = 0; i < num; i++) {
// 稍作延时,令监视曲线的变化更加明显
Thread.sleep(50);
list.add(new OOMObject());
}
System.gc();
}
public static void main(String[] args) throws Exception {
fillHeap(1000);
}
}
这段代码的作用是以 64KB/50ms 的速度向 Java 堆中填充数据,一共填充1000次,使用 JConsole 的内存页签进行监视,观察曲线和柱状指示图的变化。
程序运行后,在内存页签中可以看到内存池 Eden 区的运行趋势呈现折线状,如下图所示。监视范围扩大至整个堆后,会发现曲线是一直平滑向上增长的。从柱状图可以看到,在1000次循环执行结束,运行 System.gc()后,老年代的柱状图仍然保持峰值状态,说明被填充进堆中的数据在 System.gc() 方法执行之后仍然存活。
2.2.3 线程监控
线程页签的功能就相当于可视化的 jstack 命令了,遇到线程停顿的时候可以使用这个页签的功能进行分析。前面讲解 jstack 命令时提到线程长时间停顿的主要原因有等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁等待等,如下代码将分别演示这几种情况。
线程等待演示代码:
public class MonitoringTest {
/**
* 线程死循环演示
*/
public static void createBusyThread() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) // 第41行
;
}
}, "testBusyThread");
thread.start();
}
/**
* 线程锁等待演示
*/
public static void createLockThread(final Object lock) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "testLockThread");
thread.start();
}
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
br.readLine();
createBusyThread();
br.readLine();
Object obj = new Object();
createLockThread(obj);
}
}
程序运行后,首先在线程页签中选择 main 线程,如下图所示。堆栈追踪显示 BufferedReade r的 readBytes() 方法正在等待 System.in 的键盘输入,这时候线程为 Runnable 状态,Runnable 状态的线程仍会被分配运行时间,但 readBytes() 方法检查到流没有更新就会立刻归还执行令牌给操作系统,这种等待只消耗很小的处理器资源。
接着监控 testBusyThread 线程,如下图所示。testBusyThread 线程一直在执行空循环,从堆栈追踪中看到一直在 MonitoringTest.java 代码的16行停留,16行的代码为 while(true)。这时候线程为 Runnable 状态,而且没有归还线程执行令牌的动作,所以会在空循环耗尽操作系统分配给它的执行时间,直到线程切换为止,这种等待会消耗大量的处理器资源。
下图显示 testLockThread 线程在等待 lock 对象的 notify() 或 notifyAll() 方法的出现,线程这时候处于
WAITING 状态,在重新唤醒前不会被分配执行时间。
testLockThread 线程正处于正常的活锁等待中,只要 lock 对象的 notify() 或 notifyAll() 方法被调用,这个线程便能激活继续执行。如下代码清单演示了一个无法再被激活的死锁等待。
死锁代码样例:
public class MonitoringTest {
/**
* 线程死锁等待演示
*/
static class SynAddRunalbe implements Runnable {
int a, b;
public SynAddRunalbe(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
synchronized (Integer.valueOf(a)) {
synchronized (Integer.valueOf(b)) {
System.out.println(a + b);
}
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new SynAddRunalbe(1, 2)).start();
new Thread(new SynAddRunalbe(2, 1)).start();
}
}
}
这段代码开了 200 个线程去分别计算 1+2 以及 2+1 的值,理论上 for 循环都是可省略的,两个线程也可能会导致死锁,不过那样概率太小,需要尝试运行很多次才能看到死锁的效果。如果运气不是特别差的话,上面带 for 循环的版本最多运行两三次就会遇到线程死锁,程序无法结束。造成死锁的根本原因是 Integer.valueOf() 方法出于减少对象创建次数和节省内存的考虑,会对数值为 -128~127 之间的 Integer 对象进行缓存 ,如果valueOf() 方法传入的参数在这个范围之内,就直接返回缓存中的对象。也就是说代码中尽管调用了 200 次 Integer.valueOf() 方法,但一共只返回了两个不同的 Integer 对象。假如某个线程的两个 synchronized 块之间发生了一次线程切换,那就会出现线程A在等待被线程B持有的 Integer.valueOf(1),线程B 又在等待被线程A持有 Integer.valueOf(2),结果大家都跑不下去的情况。
出现线程死锁之后,点击 JConsole 线程面板的“检测到死锁”按钮,将出现一个新的“死锁”页签,如下图所示。
上图中很清晰地显示,线程 Thread-8 在等待一个被线程 Thread-3 持有的 Integer 对象,而点击线
程 Thread-3 则显示它也在等待一个被线程 Thread-8 持有的 Integer 对象,这样两个线程就互相卡住,除
非牺牲其中一个,否则死锁无法释放。
2.3 VisualVM:多合-故障处理工具
VisualVM(All-in-One Java Troubleshooting Tool)是功能最强大的运行监视和故障处理程序之一,曾经在很长一段时间内是 Oracle 官方主力发展的虚拟机故障处理工具。
Oracle 曾在 VisualVM 的软件说明中写上了“All-in-One”的字样,预示着它除了常规的运行监视、故障处理外,还将提供其他方面的能力,譬如性能分析(Profiling)。VisualVM 的性能分析功能比起 JProfiler、YourKit 等专业且收费的 Profiling 工具都不遑多让。而且相比这些第三方工具,VisualVM 还有一个很大的优点:不需要被监视的程序基于特殊 Agent 去运行,因此它的通用性很强,对应用程序实际性能的影响也较小,使得它可以直接应用在生产环境中。这个优点是 JProfiler、YourKit 等工具无法与之媲美的。
2.3.1 VisualVM 简介
VisualVM基于NetBeans平台开发工具,所以一开始它就具备了通过插件扩展功能的能力,有了插件扩展支持,VisualVM可以做到:
- 显示虚拟机进程以及进程的配置、环境信息(jps、jinfo)。
- 监视应用程序的处理器、垃圾收集、堆、方法区以及线程的信息(jstat、jstack)。
- dump以及分析堆转储快照(jmap、jhat)。
- 方法级的程序运行性能分析,找出被调用最多、运行时间最长的方法。
- 离线程序快照:收集程序的运行时配置、线程dump、内存dump等信息建立一个快照,可以将快
照发送开发者处进行Bug反馈。 - 其他插件带来的无限可能性。
2.3.2 VisualVM 插件安装
首次启动 VisualVM 后(管理员权限打开),读者先不必着急找应用程序进行监测,初始状态下的 VisualVM 并没有加载任何插件,虽然基本的监视、线程面板的功能主程序都以默认插件的形式提供,但是如果不在 VisualVM 上装任何扩展插件,就相当于放弃它最精华的功能,和没有安装任何应用软件的操作系统差不多。
VisualVM 的插件可以手工进行安装,在网站上下载nbm包后,点击“工具->插件->已下载”菜单,然后在弹出对话框中指定nbm包路径便可完成安装。独立安装的插件存储在 VisualVM 的根目录,譬如 JDK 9 之前自带的VisulalVM,插件安装后是放在 JDK_HOME/lib/visualvm 中的。手工安装插件并不常用,VisualVM 的自动安装功能已可找到大多数所需的插件,在有网络连接的环境下,点击“工具->插件菜单”,弹出如下图所示的插件页签,在页签的“可用插件”及“已安装”中列举了当前版本 VisualVM 可以使用的全部插件,选中插件后在右边窗口会显示这个插件的基本信息,如开发者、版本、功能描述等。
读者可根据自己的工作需要和兴趣选择合适的插件,然后点击“安装”按钮即可。
选择一个需要监视的程序就可以进入程序的主界面了,如下图所示。由于VisualVM的版本以及选择安装插件数量的不同,读者看到的页签可能和笔者的截图有所差别。
VisualVM中概述、监视、线程功能与前面介绍的 JConsole 差别不大,读者可根据上一节内容类比使用,这里笔者挑选几个有特色的功能和插件进行简要介绍。
2.3.3 生成、浏览堆转储快照
在 VisualVM 中生成堆转储快照文件有两种方式,可以执行下列任一操作:
- 在“应用程序”窗口中右键单击应用程序节点,然后选择“堆Dump”。
- 在“应用程序”窗口中双击应用程序节点以打开应用程序标签,然后在“监视”标签中单击“堆Dump”。
生成堆转储快照文件之后,应用程序页签会在该堆的应用程序下增加一个以 [heap-dump] 开头的子节点,并且在主页签中打开该转储快照,如下图所示。如果需要把堆转储快照保存或发送出去,就应在 heapdump 节点上右键选择“另存为”菜单,否则当 VisualVM 关闭时,生成的堆转储快照文件会被当作临时文件自动清理掉。要打开一个由已经存在的堆转储快照文件,通过文件菜单中的“装入”功能,选择硬盘上的文件即可。(PS:用管理员权限打开 VisualVM)
堆页签中的“摘要”面板可以看到应用程序 dump 时的运行时参数、System.getPro-perties() 的内容、线程堆栈等信息;“类”面板则是以类为统计口径统计类的实例数量、容量信息;“实例”面板不能直接使用,因为 VisualVM 在此时还无法确定用户想查看哪个类的实例,所以需要通过“类”面板进入,在“类”中选择一个需要查看的类,然后双击即可在“实例”里面看到此类的其中 500个实例的具体属性信息;“OQL控制台”面板则是运行 OQL 查询语句的,同 jhat 中介绍的 OQL 功能一样。如果读者想要了解具体 OQL 的语法和使用方法。
2.3.4 分析程序性能
在 Profiler 页签中,VisualVM 提供了程序运行期间方法级的处理器执行时间分析以及内存分析。做 Profiling 分析肯定会对程序运行性能有比较大的影响,所以一般不在生产环境使用这项功能,或者改用 JMC 来完成,JMC 的 Profiling 能力更强,对应用的影响非常轻微。
要开始性能分析,先选择“CPU”和“内存”按钮中的一个,然后切换到应用程序中对程序进行操作,VisualVM 会记录这段时间中应用程序执行过的所有方法。
- 如果是进行处理器执行时间分析,将会统计每个方法的执行次数、执行耗时;
- 如果是内存分析,则会统计每个方法关联的对象数以及这些对象所占的空间。
等要分析的操作执行结束后,点击“停止”按钮结束监控过程,如下图所示。(PS:如果点击cpu分析报错,在启动中配置了参数**-Xverify:none**)
上图是对 MonitoringTest 程序一段操作的录制和分析结果,读者分析自己的应用程序时,可根据实际业务复杂程度与方法的时间、调用次数做比较,找到最优化价值方法。
2.3.5 BTrace动态日志跟踪
BTrace 是一个很神奇的 VisualVM 插件,它本身也是一个可运行的独立程序。BTrace 的作用是在不中断目标程序运行的前提下,通过 HotSpot 虚拟机的 Instrument 功能动态加入原本并不存在的调试代码。这项功能对实际生产中的程序很有意义:如当程序出现问题时,排查错误的一些必要信息时(譬如方法参数、返回值等),在开发时并没有打印到日志之中以至于不得不停掉服务时,都可以通过调试增量来加入日志代码以解决问题。
在 VisualVM 中安装了 BTrace 插件后,在应用程序面板中右击要调试的程序,会出现“Trace Application…”菜单,点击将进入 BTrace 面板。这个面板看起来就像一个简单的Java程序开发环境,里面甚至已经有了一小段Java代码,如下图所示。
笔者准备了一段简单的 Java 代码来演示 BTrace 的功能:产生两个1000以内的随机整数,输出这两个数字相加的结果,如下代码所示。
BTrace跟踪演示
public class BTraceTest {
public int add(int a, int b) {
return a + b;
}
public static void main(String[] args) throws IOException {
BTraceTest test = new BTraceTest();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
for (int i = 0; i < 10; i++) {
reader.readLine();
int a = (int) Math.round(Math.random() * 1000);
int b = (int) Math.round(Math.random() * 1000);
System.out.println(test.add(a, b));
}
}
}
假设这段程序已经上线运行,而我们现在又有了新的需求,想要知道程序中生成的两个随机数是什么,但程序并没有在执行过程中输出这一点。此时,在 VisualVM 中打开该程序的监视,在 BTrace页签填充 TracingScript 的内容,输入调试代码,如下代码所示,即可在不中断程序运行的情况下做到这一点。
BTrace调试代码
/* BTrace Script Template */
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class TracingScript {
@OnMethod(
clazz="com.zhihong.demo.BTraceTest",
method="add",
location=@Location(Kind.RETURN)
)
public static void func(@Self com.zhihong.demo.BTraceTest instance,int a, int b,@Return int result) {
println("调用堆栈:");
jstack();
println(strcat("方法参数A:",str(a)));
println(strcat("方法参数B:",str(b)));
println(strcat("方法结果:",str(result)));
}
}
点击 Start 按钮后稍等片刻,编译完成后,Output 面板中会出现“BTrace up&running”的字样。当程序运行时将会在 Output 面板输出如下图所示的调试信息。
BTrace 的用途很广泛,打印调用堆栈、参数、返回值只是它最基础的使用形式,在它的网站上有使用 BTrace 进行性能监视、定位连接泄漏、内存泄漏、解决多线程竞争问题等的使用案例,有兴趣的读者可以去网上了解相关信息。
BTrace 能够实现动态修改程序行为,是因为它是基于 Java 虚拟机的 Instrument 开发的。Instrument 是 Java 虚拟机工具接口(Java Virtual Machine Tool Interface,JVMTI)的重要组件,提供了一套代理(Agent)机制,使得第三方工具程序可以以代理的方式访问和修改 Java 虚拟机内部的数据。阿里巴巴开源的诊断工具 Arthas 也通过 Instrument 实现了与 BTrace 类似的功能。
2.4 Java Mission Control:可持续在线的监控工具
除了大家熟知的面向通用计算(General Purpose Computing)可免费使用的 Java SE 外,Oracle 公司还开辟过带商业技术支持的 Oracle Java SE Support 和面向独立软件供应商(ISV)的 Oracle Java SE Advanced & Suite 产品线。
除去带有 7×24 小时的技术支持以及可以为企业专门定制安装包这些非技术类的增强服务外,Oracle Java SE Advanced & Suite 与普通 Oracle Java SE 在功能上的主要差别是前者包含了一系列的监控、管理工具,譬如用于企业 JRE 定制管理的 AMC(Java Advanced Management Console)控制台、JUT(Java Usage Tracker)跟踪系统,用于持续收集数据的 JFR(Java Flight Recorder)飞行记录仪和用于监控 Java 虚拟机的 JMC(Java Mission Control)。这些功能全部都是需要商业授权才能在生产环境中使用,但根据 Oracle Binary Code协议,在个人开发环境中,允许免费使用 JMC 和 JFR,本节笔者将简要介绍它们的原理和使用。
JFR 是一套内建在 HotSpot 虚拟机里面的监控和基于事件的信息搜集框架,与其他的监控工具(如 JProfiling)相比,Oracle 特别强调它“可持续在线”(Always-On)的特性。JFR在生产环境中对吞吐量的影响一般不会高于1%(甚至号称是Zero Performance Overhead),而且JFR监控过程的开始、停止都是完全可动态的,即不需要重启应用。JFR 的监控对应用也是完全透明的,即不需要对应用程序的源码做任何修改,或者基于特定的代理来运行。
JMC 最初是 BEA 公司的产品,因此并没有像 VisualVM 那样一开始就基于自家的 Net-Beans 平台来开发,而是选择了由 IBM 捐赠的 Eclipse RCP 作为基础框架,现在的 JMC 不仅可以下载到独立程序,更常见的是作为 Eclipse 的插件来使用。JMC 与虚拟机之间同样采取 JMX 协议进行通信,JMC 一方面作为 JMX 控制台,显示来自虚拟机 MBean 提供的数据;另一方面作为 JFR 的分析工具,展示来自 JFR 的数据。启动后 JMC 的主界面如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bUYI1AMU-1620034799674)(C:\Users\罗志宏\Desktop\我的博客\JVM\虚拟机监控工具\图片\JMC主界面.png)]
在左侧的“JVM浏览器”面板中自动显示了通过 JDP 协议(Java Discovery Protocol)找到的本机正在运行的HotSpot 虚拟机进程,如果需要监控其他服务器上的虚拟机,可在“文件->连接”菜单中创建远程连接,如下图所示。
这里要填写的信息应该在被监控虚拟机进程启动的时候以虚拟机参数的形式指定,以下是一份被监控端的启动参数样例:
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Djava.rmi.server.hostname=192.168.31.4
-XX:+UnlockCommercialFeatures -XX:+FlightRecorder
本地虚拟机与远程虚拟机进程的差别只限于创建连接这个步骤,连接成功创建以后的操作就是完全一样的了。把“JVM浏览器”面板中的进程展开后,可以看到每个进程的数据都有 MBean 和 JFR 两个数据来源。关于MBean 这部分数据,与 JConsole 和 VisualVM 上取到的内容是一样的,只是展示形式上有些差别,笔者就不再重复了,后面着重介绍JFR的数据记录。
双击“飞行记录器”,将会出现“启动飞行记录”窗口(如果第一次使用,还会收到解锁商业功能的警告窗),如下图所示。
// TODO