Java
Java虚拟机

Java虚拟机13 - JVM工具之VisualVM

简介:Java开发工具包中提供了众多的虚拟机工具可供开发者使用。

1. VisualVM简介

VisualVM(All-in-One Java Troubleshooting Tool)是到目前为止随JDK发布的功能最强大的运行监视和故障处理程序,并且可以预见在未来一段时间内都是官方主力发展的虚拟机故障处理工具。官方在VisualVM的软件说明中写上了“All-in-One”的描述字样,预示着它除了运行监视、故障处理外,还提供了很多其他方面的功能。如性能分析(Profiling),VisualVM的性能分析功能甚至比起JProfiler、YourKit等专业且收费的Profiling工具都不会逊色多少,而且VisualVM的还有一个很大的优点:不需要被监视的程序基于特殊Agent运行,因此它对应用程序的实际性能的影响很小,使得它可以直接应用在生产环境中。这个优点是JProfiler、YourKit等工具无法与之媲美的。

要运行VisualVM,如果JDK的目录已配置在环境变量PATH上,运行jvisualvm命令即可,下面是VisualVM的启动页面:

1.VisualVM启动页面.png

注:本案例中所使用的VisualVM基于JDK 1.8.0_45。

2. VisualVM插件安装

VisualVM基于NetBeans平台开发,因此它一开始就具备了插件扩展功能的特性,通过插件扩展支持,VisualVM可以做到:

  • 显示虚拟机进程以及进程的配置、环境信息(jpsjinfo)。
  • 监视应用程序的CPU、GC、堆、方法区以及线程的信息(jstatjstack)。
  • dump以及分析堆转储快照(jmapjhat)。
  • 方法级的程序运行性能分析,找出被调用最多、运行时间最长的方法。
  • 离线程序快照:收集程序的运行时配置、线程dump、内存dump等信息建立一个快照,可以将快照发送开发者处进行Bug反馈。

插件可以进行手工安装,在相关网站上下载后缀名.nbm包后,打开“工具”→“插件”菜单,选择“已下载”页签,点击“添加插件”,然后在弹出的对话框中指定nbm包路径便可进行安装,插件安装后存放在JDK_HOME/lib/visualvm/visualvm中。

VisualVM提供自动安装功能,在有网络连接的环境下,打开“工具”→“插件”菜单,在“可用插件”页签中列举了当前版本VisualVM可以使用的插件,选中插件后在右边窗口将显示这个插件的基本信息,如开发者、版本、功能描述等。需要安装哪个插件只需要将其勾选,然后点击左下角的“安装”即可。下面是安装了一些插件之后的页面显示:

2.VisualVM插件页面.png

3. heap堆区分析

我们编写一个测试案例,这个案例在之前也用到过,作用是不断往集合中添加对象,用这种方式来使堆内存的使用量不断上升;因为需要在测试案例程序启动后在VisualVM中连接它,因此在代码开始的地方使用标准输入来暂停程序,方便调试:

  • package com.coderap.tools.visualvm;
  • import java.io.BufferedReader;
  • import java.io.IOException;
  • import java.io.InputStreamReader;
  • import java.util.ArrayList;
  • public class HeapTest {
  • public static void main(String[] args) throws IOException, InterruptedException {
  • // 使用一个标准输入流来暂停程序,以便调试
  • BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
  • bufferedReader.readLine();
  • System.out.println("Start Run");
  • ArrayList<HeapTest> list = new ArrayList<HeapTest>();
  • while (true) {
  • // 睡眠1ms,防止内存增长过快
  • Thread.sleep(1);
  • list.add(new HeapTest());
  • }
  • }
  • }

运行程序后,在VisualVM中的左侧列表就能看到以main()方法所在的类的全限定名为名称的选项,双击它就可以开始连接,下面是连接后的监视器页签页面:

3.heap堆测试页面(1).png

此时其实堆的使用率并不高,不到25MB;当我们在程序运行的控制行中回车之后,就会开始执行后面的while死循环,不断往集合list中添加HeapTest对象,此时再观察监视器页签页面,会发现各项指标都有了变化:

4.heap堆测试页面(2).png

在程序运行期间,点击监视器页签页面右上侧的“堆 Dump”按钮,可以得到dump转储文件并自动打开一个新的页面显示分析结果,点击上面的“类”按钮,在新页面会展示实例的数量和大小,可以发现其中com.coderap.tools.visualvm.HeapTest实例的数量最多,大小最大:

5.heap堆测试页面(3).png

双击com.coderap.tools.visualvm.HeapTest这一行,会打开“实例数”页面:

6.heap堆测试页面(4).png

这个页面显示了com.coderap.tools.visualvm.HeapTest的信息。

注:对于“堆 dump”来说,在远程监控JVM的时候,VisualVM是没有这个功能的,只有本地监控的时候才有。

4. Metaspace非堆区分析

其次来看下非堆区Metaspace使用情况;运行一段类加载的程序,代码如下:

  • package com.coderap.tools.visualvm;
  • import java.io.BufferedReader;
  • import java.io.File;
  • import java.io.InputStreamReader;
  • import java.lang.reflect.Method;
  • import java.net.MalformedURLException;
  • import java.net.URL;
  • import java.net.URLClassLoader;
  • import java.util.ArrayList;
  • import java.util.List;
  • public class MetaspaceTest {
  • private static List<Object> insList = new ArrayList<Object>();
  • public static void main(String[] args) throws Exception {
  • metaspaceTest();
  • }
  • private static void metaspaceTest() throws Exception {
  • // 使用一个标准输入流来暂停程序,以便调试
  • BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
  • bufferedReader.readLine();
  • System.out.println("Start Run");
  • for (int i = 0; i < 1000; i++) {
  • URL[] urls = getURLS();
  • URLClassLoader urlClassloader = new URLClassLoader(urls, null);
  • Class<?> logfClass = Class.forName("org.apache.commons.logging.LogFactory", true, urlClassloader);
  • Method getLog = logfClass.getMethod("getLog", String.class);
  • Object result = getLog.invoke(logfClass, "MetaspaceTest");
  • insList.add(result);
  • System.out.println(i + ": " + result);
  • }
  • }
  • private static URL[] getURLS() throws MalformedURLException {
  • File libDir = new File("/Users/qinly/.m2/repository/commons-logging/commons-logging/1.1.1");
  • File[] subFiles = libDir.listFiles();
  • int count = subFiles.length;
  • URL[] urls = new URL[count];
  • for (int i = 0; i < count; i++) {
  • urls[i] = subFiles[i].toURI().toURL();
  • }
  • return urls;
  • }
  • }

运行一段时间后VisualVM监控结果如下:

7.Metaspace测试页面.png

5. CPU性能分析

CPU性能分析的主要目的是统计函数的调用情况及执行时间,即统计应用程序的CPU使用情况。在没有CPU占用的情况下,监视器页签页面的显示如下:

8.CPU性能测试页面(1).png

运行一段占用CPU较高的代码如下:

  • package com.coderap.tools.visualvm;
  • import java.io.BufferedReader;
  • import java.io.IOException;
  • import java.io.InputStreamReader;
  • public class CPUTest {
  • public static void main(String[] args) throws InterruptedException, IOException {
  • cpuDeplete();
  • }
  • public static void cpuDeplete() throws InterruptedException, IOException {
  • // 使用一个标准输入流来暂停程序,以便调试
  • BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
  • bufferedReader.readLine();
  • System.out.println("Start Run");
  • // 80%的占有率
  • int busyTime = 8;
  • // 20%的占有率
  • int idelTime = 2;
  • // 开始时间
  • long startTime = 0;
  • while (true) {
  • // 开始时间
  • startTime = System.currentTimeMillis();
  • // 运行时间
  • while (System.currentTimeMillis() - startTime < busyTime) {
  • ;
  • }
  • // 休息时间
  • Thread.sleep(idelTime);
  • }
  • }
  • }

运行一段时间后我们查看监视器页签页面的显示,可以发现此时CPU已经出现了占用情况:

9.CPU性能测试页面(2).png

过高的CPU使用率可能是由于我们的项目中存在低效的代码;在我们对程序施压的时候,过低的CPU使用率也有可能是程序的问题。

点击“抽样器”页签,点击“CPU”按钮,启动CPU性能分析会话,VisualVM会检测应用程序所有的被调用的方法,在“CPU样例”页签页面下可以看到我们的方法cpuDeplete()的自用时间最长,如下图:

10.CPU性能测试页面(3).png

切换到“线程CPU时间”页签页面下,我们的main()方法这个进程 占用CPU时间最长,如下图:

11.CPU性能测试页面(4).png

6. 线程分析篇

当我们对一个多线程应用程序进行调试或者开发后期做性能调优的时候,往往需要了解当前程序中所有线程的运行状态,如是否有死锁、热锁等情况的发生,从而分析系统可能存在的问题。在VisualVM的监视标签内,我们可以查看当前应用程序中所有活动线程(Live threads)和守护线程(Daemon threads)的数量等实时信息。

运行一段代码如下:

  • package com.coderap.tools.visualvm;
  • import java.io.BufferedReader;
  • import java.io.IOException;
  • import java.io.InputStreamReader;
  • class TestThread implements Runnable {
  • public void run() {
  • while (true) {
  • ;
  • }
  • }
  • }
  • public class ThreadTest {
  • public static void main(String[] args) throws IOException {
  • // 使用一个标准输入流来暂停程序,以便调试
  • BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
  • bufferedReader.readLine();
  • System.out.println("Start Thread - 1");
  • // 启动线程1
  • new Thread(new TestThread(), "Thread - 1").start();
  • bufferedReader.readLine();
  • System.out.println("Start Thread - 2");
  • // 启动线程2
  • new Thread(new TestThread(), "Thread - 2").start();
  • }
  • }

上面的代码在运行之后,会让用户输入任意内容,以便分别启动两个线程。在代码运行后,在“监视器”页签页面显示的线程的情况如下:

12.线程测试页面(1).png

显示此时已启动线程数为12个,守护线程数为11个。当我们在代码运行的控制台中回车后,会启动Thread - 1线程,此时线程数量会变化,已启动线程变为13:

13.线程测试页面(2).png

查看“线程”页签页面,会以时间线的方式展现线程的运行情况,如下图:

14.线程测试页面(3).png

可以看到代码里启的线程Thread - 1;再次在代码运行的控制台中回车后,会启动Thread - 2线程,此时“线程”页签页面会多出Thread - 2的时间线:

15.线程测试页面(4).png

勾选右上角的“Threads Inspector”复选框,还可以打开线程检查器,其中可以看到线程的一些情况,包括线程状态、线程所在类、线程获取的锁等情况:

16.线程测试页面(5).png

7. 检测死锁

下面是一段会引起死锁的代码:

  • package com.coderap.tools.visualvm;
  • public class DeadLockTest {
  • private static Object lockA = new Object();
  • private static Object lockB = new Object();
  • static class TestThread1 implements Runnable {
  • @Override
  • public void run() {
  • // 先获取锁lockA
  • synchronized (lockA) {
  • // 等待1s,让另一个线程成功获取到lockB
  • try {
  • Thread.sleep(1000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • // 获取锁lock,此时lockA在另一个线程手中
  • synchronized (lockB) {
  • System.out.println(Thread.currentThread().getName() + " get 2 locks");
  • }
  • }
  • }
  • }
  • static class TestThread2 implements Runnable {
  • @Override
  • public void run() {
  • // 先获取锁lockB
  • synchronized (lockB) {
  • // 等待1s,让另一个线程成功获取到lockA
  • try {
  • Thread.sleep(1000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • // 获取锁lockA,此时lockA在另一个线程手中
  • synchronized (lockA) {
  • System.out.println(Thread.currentThread().getName() + " get 2 locks");
  • }
  • }
  • }
  • }
  • public static void main(String[] args) {
  • new Thread(new TestThread1(), "Thread - 1").start();
  • new Thread(new TestThread2(), "Thread - 2").start();
  • }
  • }

运行该代码后,在VisualVM打开该进程,会发现打开的标签会闪烁,切换到“线程”页签页面,会明显展示出发现死锁的提示:

17.死锁检测测试页面(1).png

同时,勾选右上角的“Threads Inspector”线程检查器,勾选引起死锁的线程,右侧还会展示简要的线程信息:

18.死锁检测测试页面(2).png

如果想要查看详细的线程Dump信息,可以点击右上角的“线程 Dump”按钮,会出现下面的页面:

19.死锁检测测试页面(3).png

其实上面页面中的内容就是我们在使用jstack命令得到的线程快照信息。

8. JMX远程监控

要使用VisualVM进行远程监控,本机的VisualVM就必须和远程的JVM要进行通信,Visualvm目前支持两种远程连接的方式,分别是jstatd和JMX方式,这里主要介绍的是通过JMX方式。

8.1. 配置账号和密码

要连接远程的JVM虚拟机上的进程,需要先账号密码,并设置访问权限,分为以下几步:

  1. 修改需要被监控远程服务器上的JDK配置文件;

进入JAVA_HOME\jre\lib\management\,在当前目录将jmxremote.password.template拷贝一份为jmxremote.password,修改jmxremote.password文件的内容;该配置文件大部分内容为注释,最底下有两行显示:

  • ...
  • # Following are two commented-out entries. The "measureRole" role has
  • # password "QED". The "controlRole" role has password "R&D".
  • #
  • # monitorRole QED
  • # controlRole R&D

意思是这里可以添加远程登录的账号和密码。比如默认的账号是monitorRolecontrolRole,其对应的密码分别是QEDR&D。我们可以自定义用户名密码,如添加一个账号和密码都为root的配置:

  • ...
  • # Following are two commented-out entries. The "measureRole" role has
  • # password "QED". The "controlRole" role has password "R&D".
  • #
  • # monitorRole QED
  • # controlRole R&D
  • root root

注:在生产环境中,禁止使用过于简单的用户名和密码,这里只做演示。

  1. 添加访问权限

修改当前目录里的jmxremote.access文件,该文件可以控制访问权限;添加我们刚刚创建的root账户的访问权限,如图:

  • ...
  • # Default access control entries:
  • # o The "monitorRole" role has readonly access.
  • # o The "controlRole" role has readwrite access and can create the standard
  • # Timer and Monitor MBeans defined by the JMX API.
  • monitorRole readonly
  • controlRole readwrite \
  • create javax.management.monitor.*,javax.management.timer.* \
  • unregister
  • root readwrite \
  • create javax.management.monitor.*,javax.management.timer.* \
  • unregister

这里拷贝了上面的默认配置,并修改用户名为root即可。

8.2. 以可监控模式启动Java程序

在运行代码的时候,需要添加Java参数,如我在当前目录编写了一个Test.java文件并成功编译,使用命令行启动该Test程序:

  • $ > java -Djava.rmi.server.hostname=47.96.138.114 -Dcom.sun.management.jmxremote.port=8888 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=true Test

在上面的参数中,java.rmi.server.hostname表示本机的IP;com.sun.management.jmxremote.port表示本机JMX远程连接的端口,这里设置为8888;com.sun.management.jmxremote.ssl表示关闭SSL连接;com.sun.management.jmxremote.authenticate=true表示远程连接时需要认证身份。

详细的可用参数意义见下面:

启动后可以查看JMX服务是否已经监听在8888端口:

  • $ > netstat -anp | grep 8888
  • tcp 0 0 0.0.0.0:8888 0.0.0.0:* LISTEN 26525/java

如果在配置JMX远程访问的时候,设置jmxremote.password文件权限,修改该文件时添加写权限,chmod +w jmxremote.password ,放开角色信息那俩行的注释,保存,再使用chmod 0400 jmxremote.password

如果在启动时报Error: Password file read access must be restricted错误,则说明jmxremote.password文件的权限不对:

  • $ > java -Djava.rmi.server.hostname=47.96.138.114 -Dcom.sun.management.jmxremote.port=8888 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=true Test
  • Error: Password file read access must be restricted: /soft/jdk1.8.0_45/jre/lib/management/jmxremote.password

解决办法是给该文件添加写权限:

  • $ > chmod 0400 /soft/jdk1.8.0_45/jre/lib/management/jmxremote.password

8.3. 连接远程Java程序进行监控

一切就绪之后,就可以使用VisualVM来连接远程Java程序了。打开VisualVM之后,在左侧边栏的“远程”上右击选择“添加远程主机”,在弹出的对话框中填入在启动Java程序时指定的java.rmi.server.hostname,可以是IP也可以是域名:

20.远程监控——添加远程主机.png

添加完成后,左侧边栏“远程”菜单下会多出刚刚添加的主机菜单,右击选择“添加JMX连接”,填入相应的信息:

21.远程监控——添加JMX连接.png

注:需要注意的是,在某些云主机提供商提供的主机上,有些端口并没有开放,如果要使用JMX连接,那么相应的端口应该开放连接权限。

点击“连接”按钮后就会尝试进行连接,当连接成功时会在左侧之前添加的远程主机下显示连接的Java程序进程,后面的pid即是Java程序在远程主机上的进程ID;双击该选项,可以打开程序相关的信息,使用方式和本地监控是类似的,下面是相关的两个页面:

22.远程监控-连接后页面(1).png

23.远程监控-连接后页面(2).png