JDK中除了附带大量的命令行工具外,还提供了几个功能集成度更高的可视化工具,用户可以使 用这些可视化工具以更加便捷的方式进行进程故障诊断和调试工作。这类工具主要包括JConsole、 JHSDB、VisualVM和JMC四个。其中,JConsole是最古老,早在JDK 5时期就已经存在的虚拟机监控 工具,而JHSDB虽然名义上是JDK 9中才正式提供,但之前已经以sa-jdi.jar包里面的HSDB(可视化工具)和CLHSDB(命令行工具)的形式存在了很长一段时间[1]。它们两个都是JDK的正式成员,随着 JDK一同发布,无须独立下载,使用也是完全免费的。
VisualVM在JDK 6 Update 7中首次发布,直到JRockit Mission Control与OracleJDK的融合工作完成之前,它都曾是Oracle主力推动的多合一故障处理工具,现在它已经从OracleJDK中分离出来,成为一 个独立发展的开源项目[2]。VisualVM已不是JDK中的正式成员,但仍是可以免费下载、使用的。
Java Mission Control,曾经是大名鼎鼎的来自BEA公司的图形化诊断工具,随着BEA公司被Oracle 收购,它便被融合进OracleJDK之中。在JDK 7 Update 40时开始随JDK一起发布,后来Java SE Advanced产品线建立,Oracle明确区分了Oracle OpenJDK和OracleJDK的差别[3],JMC从JDK 11开始又被移除出JDK。虽然在2018年Oracle将JMC开源并交付给OpenJDK组织进行管理,但开源并不意味着 免费使用,JMC需要与HotSpot内部的“飞行记录仪”(Java Flight Recorder,JFR)配合才能工作,而在 JDK 11以前,JFR的开启必须解锁OracleJDK的商业特性支持(使用JCMD的 VM.unlock_commercial_features或启动时加入-XX:+UnlockCommercialFeatures参数),所以这项功能 在生产环境中仍然是需要付费才能使用的商业特性。
[1] 准确来说是Linux和Solaris在OracleJDK 6就可以使用HSDB和CLHSDB了,Windows上要到Oracle-JDK 7才可以用。
[2] VisualVM官方站点:https://visualvm.github.io。
[3] 详见https://blogs.oracle.com/java-platform-group/oracle-jdk-releases-for-java-11-and-later。
JHSDB:基于服务性代理的调试工具
JDK中提供了JCMD和JHSDB两个集成式的多功能工具箱,它们不仅整合了上一节介绍到的所有基础工具所能提供的专项功能,而且由于有着“后发优势”,能够做得往往比之前的老工具们更好、更 强大,表4-15所示是JCMD、JHSDB与原基础工具实现相同功能的简要对比。
表4-15 JCMD、JHSDB和基础工具的对比
| 基础工具 | JCMD | JHSDB |
|---|---|---|
| jps -lm | jcmd | N/A |
| jmap -dump <pid> | jcmd <pid> GC.heap_dump | jhsdb jmap --binaryheap |
| jmap-histo <pid> | jcmd <pid> GC.class_histogram | jhsdb jmap --histo |
| jstack <pid> | jcmd <pid> Thread.print | jhsdb jstack --locks |
| jinfo -sysprops <pid> | jcmd <pid> VM.system_properties | jhsdb info --sysprops |
| jinfo -flags <pid> | jcmd <pid> VM.flags | jhsdbj info --flags |
JHSDB是一款基于服务性代理(Serviceability Agent,SA)实现的进程外调试工具。服务性代理是 HotSpot虚拟机中一组用于映射Java虚拟机运行信息的、主要基于Java语言(含少量JNI代码)实现的 API集合。服务性代理以HotSpot内部的数据结构为参照物进行设计,把这些C++的数据抽象出Java模型对象,相当于HotSpot的C++代码的一个镜像。通过服务性代理的API,可以在一个独立的Java虚拟 机的进程里分析其他HotSpot虚拟机的内部数据,或者从HotSpot虚拟机进程内存中dump出来的转储快照里还原出它的运行状态细节。服务性代理的工作原理跟Linux上的GDB或者Windows上的Windbg是相似的。本次,我们要借助JHSDB来分析一下代码清单4-6中的代码[1],并通过实验来回答一个简单问题:staticObj、instanceObj、localObj这三个变量本身(而不是它们所指向的对象)存放在哪里?
代码清单4-6 JHSDB测试代码
/*** staticObj、instanceObj、localObj存放在哪里? */
public class JHSDB_TestCase {
static class Test {
static ObjectHolder staticObj = new ObjectHolder();
ObjectHolder instanceObj = new ObjectHolder();
void foo() {
ObjectHolder localObj = new ObjectHolder();
System.out.println("done"); // 这里设一个断点
}
}
private static class ObjectHolder {}
public static void main(String[] args) {
Test test = new JHSDB_TestCase.Test();
test.foo();
}
}
答案读者当然都知道:staticObj随着Test的类型信息存放在方法区,instanceObj随着Test的对象实 例存放在Java堆,localObject则是存放在foo()方法栈帧的局部变量表中。现在要做的是通过JHSDB来实践验证这一点。 首先,我们要确保这三个变量已经在内存中分配好,然后将程序暂停下来,以便有空隙进行实验,这只要把断点设置在代码中加粗的打印语句上,然后在调试模式下运行程序即可。由于JHSDB本 身对压缩指针的支持存在很多缺陷,建议用64位系统的读者在实验时禁用压缩指针,另外为了后续操作时可以加快在内存中搜索对象的速度,也建议读者限制一下Java堆的大小。本例中,笔者采用的运 行参数如下:
-Xmx10m -XX:+UseSerialGC -XX:-UseCompressedOops
程序执行后通过jps查询到测试程序的进程ID,具体如下:
jps -l
8440 org.jetbrains.jps.cmdline.Launcher
11180 JHSDB_TestCase
15692 jdk.jcmd/sun.tools.jps.Jps
使用以下命令进入JHSDB的图形化模式,并使其附加进程11180:
jhsdb hsdb --pid 11180
命令打开的JHSDB的界面如图4-4所示。

运行至断点位置一共会创建三个ObjectHolder对象的实例,只要是对象实 例必然会在Java堆中分配,既然我们要查找引用这三个对象的指针存放在哪里,不妨从这三个对象开 始着手,先把它们从Java堆中找出来。
首先点击菜单中的Tools->Heap Parameters[2],结果如图4-5所示,因为笔者的运行参数中指定了使 用的是Serial收集器,图中我们看到了典型的Serial的分代内存布局,Heap Parameters窗口中清楚列出了 新生代的Eden、S1、S2和老年代的容量(单位为字节)以及它们的虚拟内存地址起止范围。

如果读者实践时不指定收集器,即使用JDK默认的G1的话,得到的信息应该类似如下所示:
Heap Parameters:
garbage-first heap [0x00007f32c7800000, 0x00007f32c8200000] region size 1024K
请读者注意一下图中各个区域的内存地址范围,后面还要用到它们。打开Windows->Console窗 口,使用scanoops命令在Java堆的新生代(从Eden起始地址到To Survivor结束地址)范围内查找 ObjectHolder的实例,结果如下所示:
hsdb>scanoops 0x00007f32c7800000 0x00007f32c7b50000 JHSDB_TestCase$ObjectHolder
0x00007f32c7a7c458 JHSDB_TestCase$ObjectHolder
0x00007f32c7a7c480 JHSDB_TestCase$ObjectHolder
0x00007f32c7a7c490 JHSDB_TestCase$ObjectHolder
果然找出了三个实例的地址,而且它们的地址都落到了Eden的范围之内,算是顺带验证了一般情©©况下新对象在Eden中创建的分配规则。再使用Tools->Inspector功能确认一下这三个地址中存放的对象,结果如图4-6所示。

Inspector为我们展示了对象头和指向对象元数据的指针,里面包括了Java类型的名字、继承关 系、实现接口关系,字段信息、方法信息、运行时常量池的指针、内嵌的虚方法表(vtable)以及接口方法表(itable)等。由于我们的确没有在ObjectHolder上定义过任何字段,所以图中并没有看到任何实例字段数据,读者在做实验时不妨定义一些不同数据类型的字段,观察它们在HotSpot虚拟机里面是如何存储的。
接下来要根据堆中对象实例地址找出引用它们的指针,原本JHSDB的Tools菜单中有Compute Reverse Ptrs来完成这个功能,但在笔者的运行环境中一点击它就出现Swing的界面异常,看后台日志是报了个空指针,这个问题只是界面层的异常,跟虚拟机关系不大,所以笔者没有继续去深究,改为使用命令来做也很简单,先拿第一个对象来试试看:
hsdb> revptrs 0x00007f32c7a7c458
Computing reverse pointers...
Done.
Oop for java/lang/Class @ 0x00007f32c7a7b180
果然找到了一个引用该对象的地方,是在一个java.lang.Class的实例里,并且给出了这个实例的地 址,通过Inspector查看该对象实例,可以清楚看到这确实是一个java.lang.Class类型的对象实例,里面 有一个名为staticObj的实例字段,如图4-7所示。

从《Java虚拟机规范》所定义的概念模型来看,所有Class相关的信息都应该存放在方法区之中, 但方法区该如何实现,《Java虚拟机规范》并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。JDK 7及其以后版本的HotSpot虚拟机选择把静态变量与类型在Java语言一端的映射Class对 象存放在一起,存储于Java堆之中,从我们的实验中也明确验证了这一点[3]。接下来继续查找第二个 对象实例:
hsdb>revptrs 0x00007f32c7a7c480
Computing reverse pointers...
Done. Oop for JHSDB_TestCase$Test @ 0x00007f32c7a7c468
这次找到一个类型为JHSDB_TestCase$Test的对象实例,在Inspector中该对象实例显示如图4-8所示。

这个结果完全符合我们的预期,第二个ObjectHolder的指针是在Java堆中JHSDB_TestCase$Test对象的instanceObj字段上。但是我们采用相同方法查找第三个ObjectHolder实例时,JHSDB返回了一个 null,表示未查找到任何结果:
hsdb> revptrs 0x00007f32c7a7c490
null
看来revptrs命令并不支持查找栈上的指针引用,不过没有关系,得益于我们测试代码足够简洁, 人工也可以来完成这件事情。在Java Thread窗口选中main线程后点击Stack Memory按钮查看该线程的栈内存,如图4-9所示。

这个线程只有两个方法栈帧,尽管没有查找功能,但通过肉眼观察在地址0x00007f32e771c998上的值正好就是0x00007f32c7a7c490,而且JHSDB在旁边已经自动生成注释,说明这里确实是引用了一
个来自新生代的JHSDB_TestCase$ObjectHolder对象。
[1] 本小节的原始案例来自RednaxelaFX的博客https://rednaxelafx.iteye.com/blog/1847971。
[2] 效果与在Windows->Console中输入universe命令是等价的,JHSDB的图形界面中所有操作都可以通 过命令行完成,读者感兴趣的话,可以在控制台中输入help命令查看更多信息。
[3] 在JDK 7以前,即还没有开始“去永久代”行动时,这些静态变量是存放在永久代上的,JDK 7起把 静态变量、字符常量这些从永久代移除出去。
JConsole:Java监视与管理控制台
JConsole(Java Monitoring and Management Console)是一款基于JMX(Java Manage-ment Extensions)的可视化监视、管理工具。它的主要功能是通过JMX的MBean(Managed Bean)对系统进行信息收集和参数动态调整。JMX是一种开放性的技术,不仅可以用在虚拟机本身的管理上,还可以运行于虚拟机之上的软件中,典型的如中间件大多也基于JMX来实现管理与监控。虚拟机对JMX MBean的访问也是完全开放的,可以使用代码调用API、支持JMX协议的管理控制台,或者其他符合JMX规范的软件进行访问。
1.启动JConsole
通过JDK/bin目录下的jconsole.exe启动JCon-sole后,会自动搜索出本机运行的所有虚拟机进程,而
不需要用户自己使用jps来查询。双击选择其中一个进程便可进入主界面开始监控。 JMX支持跨服务器的管理,也可以使用下面的“远程进程”功能来连接远程服务器,对远程虚拟机进行监控。这里MonitoringTest是笔者准备的“反面教材”代码之一。双击它进入JConsole主界面,可以看到主界面里共包括“概述”、“内存”、“线程”、“类”、“VM摘要”、“MBean”六个页签,如图4-11所示。

“概述”页签里显示的是整个虚拟机主要运行数据的概览信息,包括“堆内存使用情况”、“线程”、“类”、“CPU使用情况”四项信息的曲线图,这些曲线图是后面“内存”、“线程”、“类”页签的信息汇总,具体内容将在稍后介绍。
2.内存监控
“内存”页签的作用相当于可视化的jstat命令,用于监视被收集器管理的虚拟机内存(被收集器直 接管理的Java堆和被间接管理的方法区)的变化趋势。我们通过运行代码清单4-7中的代码来体验一下 它的监视功能。
运行时设置的虚拟机参数为: -Xms100m -Xmx100m -XX:+UseSerialGC
代码清单4-7 JConsole监视代码
/*** 内存占位符对象,一个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区的运行趋势呈现折线状,如图4-12所示。

监视范围扩大至整个堆后,会发现曲线是一直平滑向上增长的。从柱状图可以看到,在1000次循环执行结束,运行了System.gc()后,虽然整个新生代Eden和Survivor区都基本被清空了,但是代表老年代的柱状图仍然保持峰值状态,说明被填充进堆中的数据在System.gc()方法执行之后仍然存活。
笔者的分析就到此为止,提两个小问题供读者思考一下,答案稍后公布。
1)虚拟机启动参数只限制了Java堆为100MB,但没有明确使用-Xmn参数指定新生代大小,读者能否从监控图中估算出新生代的容量?
2)为何执行了System.gc()之后,图4-12中代表老年代的柱状图仍然显示峰值状态,代码需要如何调整才能让System.gc()回收掉填充到堆中的对象?
问题1答案:图4-12显示Eden空间为27328KB,因为没有设置-XX:SurvivorRadio参数,所以Eden 与Survivor空间比例的默认值为8∶1,因此整个新生代空间大约为27328KB×125%=34160KB。
问题2答案:执行System.gc()之后,空间未能回收是因为List<OOMObject>list对象仍然存活, fillHeap()方法仍然没有退出,因此list对象在System.gc()执行时仍然处于作用域之内[1]。如果把 System.gc()移动到fillHeap()方法外调用就可以回收掉全部内存。
3.线程监控
如果说JConsole的“内存”页签相当于可视化的jstat命令的话,那“线程”页签的功能就相当于可视化 的jstack命令了,遇到线程停顿的时候可以使用这个页签的功能进行分析。前面讲解jstack命令时提到线程长时间停顿的主要原因有等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁等 待等,代码清单4-8将分别演示这几种情况。
代码清单4-8 线程等待演示代码
/*** 线程死循环演示 */
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线程,如图4-13所示。堆栈追踪显示BufferedReader的 readBytes()方法正在等待System.in的键盘输入,这时候线程为Runnable状态,Runnable状态的线程仍会被分配运行时间,但readBytes()方法检查到流没有更新就会立刻归还执行令牌给操作系统,这种等待只消耗很小的处理器资源。

接着监控testBusyThread线程,如图4-14所示。testBusyThread线程一直在执行空循环,从堆栈追踪中看到一直在MonitoringTest.java代码的41行停留,41行的代码为while(true)。这时候线程为Runnable 状态,而且没有归还线程执行令牌的动作,所以会在空循环耗尽操作系统分配给它的执行时间,直到线程切换为止,这种等待会消耗大量的处理器资源。

图4-15显示testLockThread线程在等待lock对象的notify()或notifyAll()方法的出现,线程这时候处于 WAITING状态,在重新唤醒前不会被分配执行时间。

testLockThread线程正处于正常的活锁等待中,只要lock对象的notify()或notifyAll()方法被调用, 这个线程便能激活继续执行。代码清单4-9演示了一个无法再被激活的死锁等待。
代码清单4-9 死锁代码样例
/*** 线程死锁等待演示 */
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对象进行缓存[2],如果valueOf()方法传入的参数在这个范围之内,就直接返回缓存中的对象。 也就是说代码中尽管调用了200次Integer.valueOf()方法,但一共只返回了两个不同的Integer对象。假如某个线程的两个synchronized块之间发生了一次线程切换,那就会出现线程A在等待被线程B持有的 Integer.valueOf(1),线程B又在等待被线程A持有的Integer.valueOf(2),结果大家都跑不下去的情况。 出现线程死锁之后,点击JConsole线程面板的“检测到死锁”按钮,将出现一个新的“死锁”页签,如图4-16所示。

图4-16中很清晰地显示,线程Thread-43在等待一个被线程Thread-12持有的Integer对象,而点击线程Thread-12则显示它也在等待一个被线程Thread-43持有的Integer对象,这样两个线程就互相卡住,除 非牺牲其中一个,否则死锁无法释放。
[1] 准确地说,只有虚拟机使用解释器执行的时候,“在作用域之内”才能保证它不会被回收,因为这里的回收还涉及局部变量表变量槽的复用、即时编译器介入时机等问题,具体读者可参考第8章的代码清 单8-1。
[2] 这是《Java虚拟机规范》中明确要求缓存的默认值,实际值可以调整,具体取决于java.lang.Integer.Integer-Cache.high参数的设置。
VisualVM:多合-故障处理工具
VisualVM(All-in-One Java Troubleshooting Tool)是功能最强大的运行监视和故障处理程序之一, 曾经在很长一段时间内是Oracle官方主力发展的虚拟机故障处理工具。Oracle曾在VisualVM的软件说明中写上了“All-in-One”的字样,预示着它除了常规的运行监视、故障处理外,还将提供其他方面的能力,譬如性能分析(Profiling)。VisualVM的性能分析功能比起JProfiler、YourKit等专业且收费的 Profiling工具都不遑多让。而且相比这些第三方工具,VisualVM还有一个很大的优点:不需要被监视的程序基于特殊Agent去运行,因此它的通用性很强,对应用程序实际性能的影响也较小,使得它可以直接应用在生产环境中。这个优点是JProfiler、YourKit等工具无法与之媲美的。
1.VisualVM兼容范围与插件安装
VisualVM基于NetBeans平台开发工具,所以一开始它就具备了通过插件扩展功能的能力,有了插件扩展支持,VisualVM可以做到:
- 显示虚拟机进程以及进程的配置、环境信息(jps、jinfo)。
- 监视应用程序的处理器、垃圾收集、堆、方法区以及线程的信息(jstat、jstack)。
- dump以及分析堆转储快照(jmap、jhat)。
- 方法级的程序运行性能分析,找出被调用最多、运行时间最长的方法。
- 离线程序快照:收集程序的运行时配置、线程dump、内存dump等信息建立一个快照,可以将快 照发送开发者处进行Bug反馈。
- 其他插件带来的无限可能性。
VisualVM在JDK 6 Update 7中首次发布,但并不意味着它只能监控运行于JDK 6上的程序,它具备很优秀的向下兼容性,甚至能向下兼容至2003年发布的JDK 1.4.2版本[1],这对无数处于已经完成实施、正在维护的遗留项目很有意义。当然,也并非所有功能都能完美地向下兼容,主要功能的兼容性 见表4-16所示。
| 特性 | JDK 1.4.2 | JDK 5 | JDK 6 local | JDK 6 remote |
|---|---|---|---|---|
| 运行环境信息 | ✔ | ✔ | ✔ | ✔ |
| 系统属性 | ✔ | |||
| 监视面板 | ✔ | ✔ | ✔ | ✔ |
| 线程面板 | ✔ | ✔ | ✔ | |
| 性能监控 | ✔ | |||
| 堆、线程Dump | ✔ | |||
| MBean管理 | ✔ | ✔ | ✔ | |
| JConsole插件 | ✔ | ✔ | ✔ |
首次启动VisualVM后,读者先不必着急找应用程序进行监测,初始状态下的VisualVM并没有加载 任何插件,虽然基本的监视、线程面板的功能主程序都以默认插件的形式提供,但是如果不在VisualVM上装任何扩展插件,就相当于放弃它最精华的功能,和没有安装任何应用软件的操作系统差不多。
VisualVM的插件可以手工进行安装,在网站[2]上下载nbm包后,点击“工具->插件->已下载”菜 单,然后在弹出对话框中指定nbm包路径便可完成安装。独立安装的插件存储在VisualVM的根目录,譬如JDK 9之前自带的VisulalVM,插件安装后是放在JDK_HOME/lib/visualvm中的。手工安装插件并不常用,VisualVM的自动安装功能已可找到大多数所需的插件,在有网络连接的环境下,点击“工具-> 插件菜单”,弹出如图4-17所示的插件页签,在页签的“可用插件”及“已安装”中列举了当前版本 VisualVM可以使用的全部插件,选中插件后在右边窗口会显示这个插件的基本信息,如开发者、版 本、功能描述等。


2.生成、浏览堆转储快照
在VisualVM中生成堆转储快照文件有两种方式,可以执行下列任一操作:
- 在“应用程序”窗口中右键单击应用程序节点,然后选择“堆Dump”。
- 在“应用程序”窗口中双击应用程序节点以打开应用程序标签,然后在“监视”标签中单击“堆Dump”。
生成堆转储快照文件之后,应用程序页签会在该堆的应用程序下增加一个以[heap-dump]开头的子节点,并且在主页签中打开该转储快照,如图4-20所示。如果需要把堆转储快照保存或发送出去,就应在heapdump节点上右键选择“另存为”菜单,否则当VisualVM关闭时,生成的堆转储快照文件会被当作临时文件自动清理掉。要打开一个由已经存在的堆转储快照文件,通过文件菜单中的“装入”功能,选择硬盘上的文件即可。

堆页签中的“摘要”面板可以看到应用程序dump时的运行时参数、System.getProperties()的内容、 线程堆栈等信息;“类”面板则是以类为统计口径统计类的实例数量、容量信息;“实例”面板不能直接使用,因为VisualVM在此时还无法确定用户想查看哪个类的实例,所以需要通过“类”面板进入, 在“类”中选择一个需要查看的类,然后双击即可在“实例”里面看到此类的其中500个实例的具体属性信 息;“OQL控制台”面板则是运行OQL查询语句的,同jhat中介绍的OQL功能一样。如果读者想要了解具体OQL的语法和使用方法,可参见本书附录D的内容。
3.分析程序性能
在Profiler页签中,VisualVM提供了程序运行期间方法级的处理器执行时间分析以及内存分析。做 Profiling分析肯定会对程序运行性能有比较大的影响,所以一般不在生产环境使用这项功能,或者改用 JMC来完成,JMC的Profiling能力更强,对应用的影响非常轻微。
要开始性能分析,先选择“CPU”和“内存”按钮中的一个,然后切换到应用程序中对程序进行操作,VisualVM会记录这段时间中应用程序执行过的所有方法。如果是进行处理器执行时间分析,将会统计每个方法的执行次数、执行耗时;如果是内存分析,则会统计每个方法关联的对象数以及这些对象所占的空间。等要分析的操作执行结束后,点击“停止”按钮结束监控过程,如图4-21所示。

注意 在JDK 5之后,在客户端模式下的虚拟机加入并且自动开启了类共享——这是一个在多虚拟机进程共享rt.jar中类数据以提高加载速度和节省内存的优化,而根据相关Bug报告的反映, VisualVM的Profiler功能会因为类共享而导致被监视的应用程序崩溃,所以读者进行Profiling前,最好在被监视程序中使用-Xshare:off参数来关闭类共享优化。
4.BTrace动态日志跟踪
BTrace[3]是一个很神奇的VisualVM插件,它本身也是一个可运行的独立程序。BTrace的作用是在不中断目标程序运行的前提下,通过HotSpot虚拟机的Instrument功能[4]动态加入原本并不存在的调试代码。这项功能对实际生产中的程序很有意义:如当程序出现问题时,排查错误的一些必要信息时(譬如方法参数、返回值等),在开发时并没有打印到日志之中以至于不得不停掉服务时,都可以通过调试增量来加入日志代码以解决问题。
在VisualVM中安装了BTrace插件后,在应用程序面板中右击要调试的程序,会出现“Trace Application…”菜单,点击将进入BTrace面板。这个面板看起来就像一个简单的Java程序开发环境,里面甚至已经有了一小段Java代码,如图4-22所示。

笔者准备了一段简单的Java代码来演示BTrace的功能:产生两个1000以内的随机整数,输出这两 个数字相加的结果,如代码清单4-10所示。
代码清单4-10 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的内容,输入调试代码,如代码清单4-11所示,即可在不中断程序运行的情况下做到这一点。
代码清单4-11 BTrace调试代码
/* BTrace Script Template */
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class TracingScript {
@OnMethod( clazz="org.fenixsoft.monitoring.BTraceTest", method="add", location=@Location(Kind.RETURN) )
public static void func(@Self org.fenixsoft.monitoring.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 code successfuly deployed”的字样。当程序运行时将会在Output面板输出如图4-23所示的调试信息。

BTrace的用途很广泛,打印调用堆栈、参数、返回值只是它最基础的使用形式,在它的网站上有使用BTrace进行性能监视、定位连接泄漏、内存泄漏、解决多线程竞争问题等的使用案例,有兴趣的读者可以去网上了解相关信息。
BTrace能够实现动态修改程序行为,是因为它是基于Java虚拟机的Instrument开发的。Instrument是Java虚拟机工具接口(Java Virtual Machine Tool Interface,JVMTI)的重要组件,提供了一套代理 (Agent)机制,使得第三方工具程序可以以代理的方式访问和修改Java虚拟机内部的数据。阿里巴巴开源的诊断工具Arthas也通过Instrument实现了与BTrace类似的功能。
[1] 早于JDK 6的平台,需要打开-Dcom.sun.management.jmxremote参数才能被VisualVM管理。
[2] 插件中心地址:https://visualvm.github.io/pluginscenters.html。
[3] 官方主页:https://github.com/btraceio/btrace。
[4] 是JVMTI中的主要组成部分,HotSpot虚拟机允许在不停止运行的情况下,更新已经加载的类的代码。
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[1]与普通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的主界面如图4-24所示。

JDK提供了JConsole、JHSDB、VisualVM和JMC等可视化工具,用于进程故障诊断和调试。JConsole基于JMX,JHSDB基于服务性代理,VisualVM功能强大且通用性强,还支持插件扩展,JMC需商业授权,JFR可持续在线监控,对应用影响小。
1310

被折叠的 条评论
为什么被折叠?



