Java虚拟机(五):性能监控和分析工具

1 概述

在之前的文章,我们了解了Java内存布局、内存模型、对象模型和垃圾回收的知识。在实际生产中,绝大多数Java开发者都不会参与到JVM的开发中,那我们为什么要理解JVM呢?因为我们要排查、解决生产中出现的问题,在理解了理论知识的基础上,使用一些辅助的工具,才能更快、更准的定位问题,并且更好的解决问题。所以,接下来我们一起讨论一下一些常见的性能监控和分析工具。

2 JDK自带的工具

JDK自带了很多优秀,使用简单的工具,例如Jps,Jconsole,Jstat,Jstack等,都能在JDK目录下的bin目录找到,windows下的是.exe可执行文件,在unix系系统下的是.sh脚本文件,可以直接用vim打开。

笔者使用的windows操作系统,下面的相关工具的使用都是在windows下进行的,可能和unix系统有些许差异。

2.1 Jps

Jps即Java Process Status Tool的简称,从名称可以看到,应该是类似进程管理器的东西,实际上差不多,但是从功能上来看,还不足以称做“管理器”,因为Jps没有提供关闭,杀死进程等功能,仅仅提供了查看Java虚拟机进程的功能。

Jps的命令格式如下所示:

usage: jps [-help]
       jps [-q] [-mlvV] []
Definitions:
    :      [:]

试着执行一下:

10320 sun.tools.jps.Jps
8756 org.jetbrains.jps.cmdline.Launcher
11288
2572 org/netbeans/Main

最前面的是虚拟机进程号,这个比较重要,因为后面的各种工具要连接的时候都需要知道虚拟机进程号,所以一般Jps除了查看当前有哪些进程之外,还有一个目的就是获取进程号,通过-l参数列出主类,可以知道该进程是由哪个主类(含有main的类)启动的。

iuL5B4.png

上图是options的可选项,大家试试就知道了,比较简单,就不再赘述了。

2.2 Jstat

Jstat是用于监视虚拟机各种状态信息的命令行工具,包括类加载,GC,内存等信息,没有提供GUI界面。其格式如下所示:

invalid argument count
Usage: jstat -help|-options
       jstat -

在使用之前,我们得先用Jps获取虚拟机进程ID,我在本机上启动了一个Java进程,用Jps获取的进程ID是14220。接下来执行jstat命令:

> jstat -gcutil 14220
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00   0.00   0.00   1.04  71.42  74.38      2    0.046     1    0.002    0.048

其中S0和S1是Survivor区,E是Eden,O是Old,M是MetaSpace(Java8之后有的元空间,就是以前的方法区),CCS压缩类空间,YGC是新生代GC的次数,YGCT是新生代GC消耗的时间,FGC是Full GC的次数,FGCT是Full GC消耗的时间,GCT是GC消耗的总时间(FGCT + YGCT)。

其他主要的选项如下图所示:

iuOK5n.png

2.3 Jinfo

Jinfo的作用是实时的查看和修改虚拟机的各项参数。其格式如下所示:

Usage:
    jinfo [option] 
        (to connect to running process)
    jinfo [option] 
        (to connect to a core file)
    jinfo [option] [server_id@]
        (to connect to remote debug server)

where 

主要就是-flag参数,下面是一个使用示例:

> jinfo  -flag CMSInitiantingOccupancyFraction 14220
-XX:CMSInitiatingOccupancyFraction=-1

2.4 Jmap

Jmap用于生成堆转储快照,即dump文件,除了获取dump文件,Jmap还可以查看堆,垃圾收集器等信息。其格式如下所示:

Usage:
    jmap [option] 
        (to connect to running process)
    jmap [option] 
        (to connect to a core file)
    jmap [option] [server_id@]
        (to connect to remote debug server)

where 

执行如下命令:

> jmap -heap 14220
Mark Sweep Compact GC

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 104857600 (100.0MB)
   NewSize                  = 34930688 (33.3125MB)
   MaxNewSize               = 34930688 (33.3125MB)
   OldSize                  = 69926912 (66.6875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
   capacity = 31457280 (30.0MB)
   used     = 0 (0.0MB)
   free     = 31457280 (30.0MB)
   0.0% used
Eden Space:
   capacity = 27983872 (26.6875MB)
   used     = 0 (0.0MB)
   free     = 27983872 (26.6875MB)
   0.0% used
From Space:
   capacity = 3473408 (3.3125MB)
   used     = 0 (0.0MB)
   free     = 3473408 (3.3125MB)
   0.0% used
To Space:
   capacity = 3473408 (3.3125MB)
   used     = 0 (0.0MB)
   free     = 3473408 (3.3125MB)
   0.0% used
tenured generation:
   capacity = 69926912 (66.6875MB)
   used     = 725560 (0.6919479370117188MB)
   free     = 69201352 (65.99555206298828MB)
   1.03759765625% used

1764 interned Strings occupying 157712 bytes.

第一行就显示了使用哪种垃圾收集器,在我这里是Mark Sweep Compact,即CMS。其他信息包含了堆的新生代,老年代的空间大小,使用量等。下图是其他的一些主要参数:

iuOLIs.png

2.5 Jhat

Jhat用来配合Jmap使用的,用于分析Jmap生成的堆快照。命令格式如下所示:

Usage:  jhat [-stack ] [-refs ] [-port ] [-baseline ] [-debug ] [-version] [-h|-help] 

        -J          Pass  directly to the runtime system. For
                          example, -J-mx512m to use a maximum heap size of 512MB
        -stack false:     Turn off tracking object allocation call stack.
        -refs false:      Turn off tracking of references to objects
        -port :     Set the port for the HTTP server.  Defaults to 7000
        -exclude :  Specify a file that lists data members that should
                          be excluded from the reachableFrom query.
        -baseline : Specify a baseline object dump.  Objects in
                          both heap dumps with the same ID and same class will
                          be marked as not being "new".
        -debug :     Set debug level.
                            0:  No debug output
                            1:  Debug hprof file parsing
                            2:  Debug hprof file parsing, no server
        -version          Report version number
        -h|-help          Print this help and exit
                    The file to read

For a dump file that contains multiple heap dumps,
you may specify which dump in the file
by appending "#" to the file name, i.e. "foo.hprof#3".

All boolean options default to "true"

这个是提供GUI界面的,不过一般不会用在生产环境,一个原因分析dump文件时一件非常消耗资源的事,如果在生产环境上做了可能会导致一些资源方面的问题,另一个原因是Jhat分析的内容比较简陋,有用信息比较少。

在使用之前,我们得先使用Jmap生成一份dump文件,如下所示:

> jmap -dump:format=b,file=test.bin 14220
Dumping heap to C:\Users\72419\Desktop\test.bin ...
Heap dump file created

然后使用jhat读入该文件,并分析:

> jhat test.bin
Reading from test.bin...
Dump file created Mon Sep 24 14:50:11 CST 2018
Snapshot read, resolving...
Resolving 10303 objects...
Chasing references, expect 2 dots..
Eliminating duplicate references..
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

从日志可以看到,Jhat启动了一个HTTP服务,在7000端口监听,启动浏览器,输入http://localhost:7000/ 即可进入GUI界面。如下所示:

iuXnsO.png

点击进去慢慢探索吧。

2.6 Jstack

Jstack用于生成虚拟机当前时刻的线程快照,生成该快照的目的是为了定位线程问题,例如死锁,活锁,死循环或者请求外部资源导致的长时间等待等,通过分析快照的堆栈信息,就可以快速定位代码中的哪个类,哪个方法,甚至哪行出现了问题。下面是我写的一个死锁程序,启动之后,我们通过Jstack来查看线程快照,然后定位问题:

public class DeadLockTest {

    static class DeadLock extends Thread {

        private final String lockA;
        private final String lockB;

        public DeadLock(String lockA, String lockB) {
            this.lockA = lockA;
            this.lockB = lockB;
        }

        @Override
        public void run() {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + " get lock " + lockA);
                try {
                    Thread.sleep(500);   //停顿一下,让另一个线程运行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + " get lock " + lockB);
                }
            }
        }
    }

    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";
        Thread thread1 = new DeadLock(lockA, lockB);
        Thread thread2 = new DeadLock(lockB, lockA);

        thread1.start();
        thread2.start();

    }
}

简单解释一下程序,启动两个线程,这两个线程都要获取A和B两把锁,但是获取的顺序不同。这样就导致了线程1获取了A锁,等待B锁,但是B锁已经被线程2获取了,并且线程2也在等待A锁,最终导致他们都无法获取到锁,也就无法继续执行,导致了死锁。

使用Jstack来生成快照尝试分析:

> jstack -l 12432
......
"Thread-1" #12 prio=5 os_prio=0 tid=0x00000000195bd000 nid=0x5e90 waiting for monitor entry [0x000000001a20f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at top.yeonon.ch10.DeadLockTest$DeadLock.run(DeadLockTest.java:29)
        - waiting to lock <0x00000000d5fc0588> (a java.lang.String)
        - locked <0x00000000d5fc05c0> (a java.lang.String)

   Locked ownable synchronizers:
        - None

"Thread-0" #11 prio=5 os_prio=0 tid=0x00000000195ba000 nid=0x5ed4 waiting for monitor entry [0x000000001a10f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at top.yeonon.ch10.DeadLockTest$DeadLock.run(DeadLockTest.java:29)
        - waiting to lock <0x00000000d5fc05c0> (a java.lang.String)
        - locked <0x00000000d5fc0588> (a java.lang.String)

   Locked ownable synchronizers:
        - None
......
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000000357ccf8 (object 0x00000000d5fc0588, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000000357b858 (object 0x00000000d5fc05c0, a java.lang.String),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at top.yeonon.ch10.DeadLockTest$DeadLock.run(DeadLockTest.java:29)
        - waiting to lock <0x00000000d5fc0588> (a java.lang.String)
        - locked <0x00000000d5fc05c0> (a java.lang.String)
"Thread-0":
        at top.yeonon.ch10.DeadLockTest$DeadLock.run(DeadLockTest.java:29)
        - waiting to lock <0x00000000d5fc05c0> (a java.lang.String)
        - locked <0x00000000d5fc0588> (a java.lang.String)

Found 1 deadlock.

信息比较多,而且我已经省略了一些,主要看Found one Java-level deadlock后面的内容吧,这说明Jstack发现了一个死锁,并且将死锁相关的线程堆栈信息打印出来了,从上面可以看到“Thread-1”线程拿到了<0x00000000d5fc05c0>锁,正在等待 <0x00000000d5fc0588>锁,“Thread-0”拿着<0x00000000d5fc0588>锁,正在等待 <0x00000000d5fc05c0>,显然就是死锁了,同时看堆栈可以看到问题发生在DeadLockTest.java:29,即该类的29行出现问题(虽然这个值不一定完全对应源码位置,但基本相差不大,也算是有用的信息),接下来就是到源码相应的位置去分析代码了。

Jstack的其他主要参数选项如下所示:

iuXsWq.png

2.7 Jconsole

Jconsole是一个可视化工具,命令行输入jconsole,打开之后就看到GUI界面了,界面大致如下所示:

iuXWmF.png

每个标签的意义也比较明显,可以试着自己玩玩,比较简单,不再赘述。

我之前有一篇文章中简单提到过一些Jconsole的使用,可以去看看。理解Java内存泄露

3 第三方工具

3.1 VisualVM

VisualVM是一款非常强大的性能监控和分析工具,集合了多项功能,包括导出dump文件,分析dump文件,监控线程状态,监控虚拟机各项状态指标等,还提供了诸多丰富的插件给用户使用。

使用之前得先去官网下载,下载安装、简单使用就不多说了,网上有很多文章介绍,本文只讨论一个插件:BTrace。

Btrace是一个很有趣的插件,他可以在运行时插入一些调试代码,利用这个插件我们就可以实现不停止程序就能进行简单调试的功能。先写一些代码用来测试:

public class BTraceTest {

    public int add(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        BTraceTest bTraceTest = new BTraceTest();
        Scanner scanner = new Scanner(System.in);
        for (int i = 0; i < 10; i++) {
            scanner.nextLine();
            int a = (int) Math.round(Math.random() * 1000);
            int b = (int) Math.round(Math.random() * 1000);
            System.out.println(bTraceTest.add(a, b));
        }
    }
}

就是加分运算而已,使用Scanner是为了慢慢的看日志变化,感受BTrace的功能。启动程序之后,到VisualVM的侧边栏看看Local选项,点击后选中刚刚运行的程序,如下所示:

iujR3t.png

右键点击,选中Trace application,之后可以在右边看到一个编译器,可以在里面编写Java代码,这些代码就是用于跟踪调试源代码的,如下图所示:

iujIHg.png

加入如下代码:

/* BTrace Script Template */
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;

@BTrace
public class TracingScript {
    /* put your code here */
    @OnMethod(clazz="top.yeonon.ch10.BTraceTest", method="add", location=@Location(Kind.RETURN))
    public static void func(@Self top.yeonon.ch10.BTraceTest instance, int a, int b, @Return int result) {
        println("调用堆栈:");
        jstack();
        println(strcat("方法参数A:",str(a)));    
        println(strcat("方法参数B:",str(b)));    
        println(strcat("返回值result:",str(result)));    
    }
}

然后点击上方的start按钮,启动BTrace,等看到下方控制台中出现Done字样的时候,我们就到IDE里随便输入字符(多输入几次),然后再回到VisualVM控制台中看看结果,大致内容如下:

iujTEQ.png

这就完成了一次代码动态调试,是不是感觉很屌。BTrace还有很多其他功能,详细可以到网上去搜索。

4 小结

对虚拟机进行监控,调优以及出现问题的时候定位问题,解决问题是我们学习Java虚拟机的目的,在掌握了虚拟机相关的知识理论之后再利用辅助工具,才能更好的发现问题,解决问题。本文介绍了诸多工具,每种工具都有优点和不足,了解这些工具的特性,多多尝试使用,才能更好的掌握它们。

5 参考资料

《深入理解Java虚拟机》

你可能感兴趣的:(Java虚拟机(五):性能监控和分析工具)