在企业级 Java 应用开发中,Java 程序运行过程中会经常遇到内存不足、内存泄露、线程死锁、CPU 高占用等问题。

部分问题在日常开发中可能会被忽视或被别变通的方法绕开(比如重启服务或者调大内存),而不被深究问题的根源,如何理解并解决这些问题需要我们学会使用一些 JVM 性能调优监控工具。

本文将简单介绍常用的 JVM 性能调优监控工具:jps、jinfo、jmap、jstat 和 jstack。

1. jps

    jps(Java Virtual Machine Process Status Tool)命令用于输出 JVM 中运行的进程状态信息。命令格式如下:


        jps [-q] [-mlvV] [<hostid>]


        jps [-help]


    参数说明:


        -q:不输出应用程序主类的名称、JAR文件名和传递给main()方法的参数,只显示 pid;

        -mlvV:可以指定这些参数的任意组合,各自的作用如下:


            -m:输出传递给 main 方法的参数,如果是内嵌的 JVM 则输出为 null;

            -l:输出应用程序主类的完整包名,或者是应用程序 JAR 文件的完整路径;

            -v:输出传给 JVM 的参数;

            -V:输出通过标记的文件传递给 JVM 的参数(.hotspotrc 文件,或者是通过参数 -XX:Flags= 指定的文件);


        hostid:指定的远程主机,可以是 IP 地址和域名, 也可以指定具体协议、端口。如果不指定,则显示本机的 Java 虚拟机的进程信息;

        -help:显示 jps 命令的帮助信息。


    示例:


        以 Windows 10 的 Java 环境为例,在 D:\demo 目录下创建 App.java 文件,内容如下:

public class App {
                public static void main(String[] args) {
                    try {
                        int i = 0;
                        while (i++ < 120) {
                            System.out.println("i = " + i);
                            Thread.sleep(1000);
                        }
                    } catch (InterruptedException e) {
                        System.out.println("interrupted");
                    }
                }
            }

 

        在命令行控制台下,命令行编译运行:

D:\demo>javac App.java

            D:\demo>java App -a

                i = 1
                i = 2
                i = 3
                ...

 

        App 程序运行 120 秒,在 App 结束前,打开一个新的命令行控制台,运行如下命令:

C:\Users\admin>jps

                17528 Jps
                17388 App
                
            C:\Users\admin>jps -m -l

                17388 App -a
                19324 sun.tools.jps.Jps -m -l

        注:jps 运行了两次,所以两次的 pid 不一样,App 是同一个进程,所以 pid 都是 17388。

 

2. jinfo

    jinfo (Java Virtual Machine Configuration Information) 命令用于查看 Java 进程运行的 JVM 参数,命令格式如下:

        jinfo [option] <pid>

        jinfo [option] <executable <core>

        jinfo [option] [server_id@]<remote server IP or hostname>

    参数 option:

        -flag <name>         输出指定 VM 参数的值
        -flag [+|-]<name>    开启/关闭指定 VM 参数
        -flag <name>=<value> 设置指定 VM 参数的值
        -flags               输出全部 VM 参数的值
        -sysprops            输出 Java 系统属性
        <no option>          输出以上全部属性
        -h | -help           输出帮助信息

    示例:

        以上文的 App 程序为例,运行 App:

D:\demo>java App -a

                i = 1
                i = 2
                i = 3
                ...

        App 程序运行 120 秒,在 App 运行结束前,打开一个新的命令行控制台,运行如下命令:

C:\Users\admin>jps

                1944 App
                488 Jps
 
            C:\Users\admin>jinfo -flags 1944

                Attaching to process ID 1944, please wait...
                Debugger attached successfully.
                Server compiler detected.
                JVM version is 25.121-b13
                Non-default VM flags: -XX:CICompilerCount=4 -XX:InitialHeapSize=266338304 -XX:MaxHeapSize=4242538496 -XX:MaxNewSize=1414004736 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=88604672 -XX:OldSize=177733632 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
                Command line:

            C:\Users\admin>jinfo -flag InitialHeapSize 1944

                -XX:InitialHeapSize=266338304

            C:\Users\admin>jinfo -sysprops 1944

                Attaching to process ID 1944, please wait...
                Debugger attached successfully.
                Server compiler detected.
                JVM version is 25.121-b13
                java.runtime.name = Java(TM) SE Runtime Environment
                java.vm.version = 25.121-b13
                sun.boot.library.path = C:\Program Files\Java\jre1.8.0_121\bin
                java.vendor.url = http://java.oracle.com/
                java.vm.vendor = Oracle Corporation
                path.separator = ;
                file.encoding.pkg = sun.io
                java.vm.name = Java HotSpot(TM) 64-Bit Server VM
                sun.os.patch.level =
                sun.java.launcher = SUN_STANDARD
                user.script =
                user.country = CN
                user.dir = D:\demo
                java.vm.specification.name = Java Virtual Machine Specification
                java.runtime.version = 1.8.0_121-b13
                java.awt.graphicsenv = sun.awt.Win32GraphicsEnvironment
                os.arch = amd64
                java.endorsed.dirs = C:\Program Files\Java\jre1.8.0_121\lib\endorsed
                line.separator =

                ...

            # 查看是否有 PrintGC 参数
            C:\Users\admin>jinfo -flag PrintGC 1944
            -XX:-PrintGC

            # 开启 PrintGC 参数
            C:\Users\admin>jinfo -flag +PrintGC 1944

            C:\Users\admin>jinfo -flag PrintGC 1944
            -XX:+PrintGC

            # 关闭 PrintGC 参数
            C:\Users\admin>jinfo -flag -PrintGC 1944

            C:\Users\admin>jinfo -flag PrintGC 1944
            -XX:-PrintGC

3. jmap

    jmap(Java Virtual Machine Memory Map)命令用于生成 Java 虚拟机(JVM)的堆转储快照 dump 文件。此外,jmap 命令还可以查看 finalize 执行队列、Java 堆和方法区的详细信息,比如空间使用率、当前使用的什么垃圾回收器、分代情况等等。命令格式如下:

        jmap [option] <pid>

    参数 option:

        -heap:显示 Java 堆的信息;
        -histo[:live]:显示 Java 堆中对象的统计信息,包括:对象数量、占用内存大小(单位:字节)和类的完全限定名。
        
            live 子参数可选,如果指定,则只计算活动的对象;

        -clstats:显示 Java 堆中元空间的类加载器的统计信息;
        -finalizerinfo:显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象;
        -dump:[live,]format=b,file=:生成 Java 虚拟机的堆转储快照 dump 文件。具体说明如下:

            live 子参数是可选,如果指定,则只转储堆中的活动对象;
            format=b 表示以 hprof 二进制格式转储 Java 堆的内存。
            file=<filename> 用于指定快照 dump 文件的文件名。

        -F:强制模式。如果指定的 pid 没有响应,可以配合 -dump 或 -histo 一起使用。此模式下,不支持 live 参数。
        -h 和 -help:显示 jinfo 命令的帮助信息。
        <no option>:jinfo 命令会显示 Java 虚拟机进程的内存映像信息。

     示例:

        以上文的 App 程序为例,运行 App:

D:\demo>java App -a

                i = 1
                i = 2
                i = 3
                ...

        App 程序运行 120 秒,在 App 运行结束前,打开一个新的命令行控制台,运行如下命令:

C:\Users\admin>jps

                18996 Jps
                9128 App

            C:\Users\admin>jmap -heap 9128

                Attaching to process ID 9128, please wait...
                Debugger attached successfully.
                Server compiler detected.
                JVM version is 25.121-b13

                using thread-local object allocation.
                Parallel GC with 8 thread(s)

                Heap Configuration:
                    MinHeapFreeRatio         = 0
                    MaxHeapFreeRatio         = 100
                    MaxHeapSize              = 4242538496 (4046.0MB)
                    NewSize                  = 88604672 (84.5MB)
                    MaxNewSize               = 1414004736 (1348.5MB)
                    OldSize                  = 177733632 (169.5MB)
                    NewRatio                 = 2
                    SurvivorRatio            = 8
                    MetaspaceSize            = 21807104 (20.796875MB)
                    CompressedClassSpaceSize = 1073741824 (1024.0MB)
                    MaxMetaspaceSize         = 17592186044415 MB
                    G1HeapRegionSize         = 0 (0.0MB)

                Heap Usage:
                PS Young Generation

                ...

                1601 interned Strings occupying 148128 bytes.

            C:\Users\admin>jmap -histo:live 9128


                num     #instances         #bytes  class name
                ----------------------------------------------
                1:          2273         394784  [C
                2:           487          55624  java.lang.Class
                3:          2003          48072  java.lang.String
                4:           837          33480  java.util.TreeMap$Entry
                5:           515          29560  [Ljava.lang.Object;

                ...

                Total          7813         655544

            C:\Users\admin>jmap -dump:live,format=b,file=d:\heap_test.bin 9128

                Dumping heap to D:\heap_test.bin ...
                Heap dump file created

4. jstat

    jstat(Java Virtual Machine Statistics Monitoring Tool)命令用于查看 Java 虚拟机(JVM)的运行状态信息,它可以显示 Java 虚拟机中的类加载、内存、垃圾收集、即时编译等运行状态信息。命令格式如下:

        jstat -help|-options
        
        jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]] <pid>

    参数说明:

        -help:显示帮助信息。
        -options:显示输出选项参数的列表。
        -<option>:输出选项,指定显示某一种 Java 虚拟机信息。默认以输出选项决定 jstat 命令显示的内容和格式,选项如下:

            -class:显示类加载、卸载数量、总空间和装载耗时的统计信息。
            -compiler:显示即时编译的方法、耗时等信息。
            -gc:显示堆各个区域内存使用和垃圾回收的统计信息。
            -gccapacity:显示堆各个区域的容量及其对应的空间的统计信息。
            -gcutil:显示有关垃圾收集统计信息的摘要。
            -gccause:显示关于垃圾收集统计信息的摘要(与-gcutil相同),以及最近和当前垃圾回收的原因。
            -gcnew:显示新生代的垃圾回收统计信息。
            -gcnewcapacity:显示新生代的大小及其对应的空间的统计信息。
            -gcold: 显示老年代和元空间的垃圾回收统计信息。
            -gcoldcapacity:显示老年代的大小统计信息。
            -gcmetacapacity:显示元空间的大小的统计信息。
            -printcompilation:显示即时编译方法的统计信息。

        -t:把时间戳列显示为输出的第一列。这个时间戳是从 Java 虚拟机的开始运行到现在的秒数。
        -h n:每显示 n 行显示一次表头,其中 n 为正整数。默认值为 0,即仅在第一行数据显示一次表头。
        vmid:虚拟机唯一 ID(LVMID,Local Virtual Machine Identifier),如果查看本机就是 Java 进程的进程 ID。
        interval:显示信息的时间间隔,单位默认毫秒。也可以指定秒为单位,比如:1s。如果指定了该参数,jstat 命令将每个这段时间显示一次统计信息。
        count:显示数据的次数,默认值是无穷大,这将导致 jstat 命令一直显示统计信息,直到目标JVM终止或 jstat 命令终止。

    示例:

        以上文的 App 程序为例,运行 App:

D:\demo>java App -a

                i = 1
                i = 2
                i = 3
                ...

        App 程序运行 120 秒,在 App 运行结束前,打开一个新的命令行控制台,运行如下命令:

C:\Users\admin>jps

                22436 App
                17944 Jps
            
            C:\Users\admin>jstat -class 22436

                Loaded  Bytes  Unloaded  Bytes     Time
                   420   856.9        0     0.0       0.03   

            C:\Users\admin>jstat -compiler -t  22436         

                Timestamp    Compiled Failed Invalid   Time   FailedType FailedMethod
                     63.4         35      0       0     0.01          0

            C:\Users\admin>jstat -gcnew 22436

                S0C    S1C    S0U    S1U   TT MTT  DSS      EC       EU     YGC     YGCT
                10752.0 10752.0    0.0    0.0 15  15    0.0  65024.0   2601.0      0    0.000

5. jstack

    jstack(Java Virtual Machine Stack Trace)用于生成 Java 虚拟机当前时刻的线程快照信息。线程快照一般被称为 thread dump 或者 javacore 文件,是当前 Java 虚拟机中每个线程正在执行的 Java 线程、虚拟机内部线程和可选的本地方法堆栈帧的集合。
    
    对于每个方法栈帧,将会显示完整的类名、方法名、字节码索引 (bytecode index,BCI) 和行号。生成的线程快照可以用于定位线程出现长时间停顿的原因,比如:线程间死锁、死循环、请求外部资源被长时间挂起等信息。
    
    命令格式如下:

        jstack [-l] <pid>

        jstack -F [-m] [-l] <pid>

        jstack [-m] [-l] <executable> <core>

        jstack [-m] [-l] [server_id@]<remote server IP or hostname>

    参数说明:

        -F:当正常请求不被响应时 (挂起),强制输出线程快照信息;
        -m:显示 Java 方法栈帧和本地方法栈帧(混合模式),本地方法栈帧是 C 或 C++ 编写的虚拟机代码或 JNI/native 代码;        
        -l:显示关于锁的附加信息,比如属于 java.util.concurrent 的 ownable synchronizers 列表;
        -h 和 -help:显示 jstack 命令的帮助信息。

    示例:

       以上文的 App 程序为例,运行 App:

D:\demo>java App -a

                i = 1
                i = 2
                i = 3
                ...

        App 程序运行 120 秒,在 App 运行结束前,打开一个新的命令行控制台,运行如下命令:

C:\Users\admin>jps

                13844 Jps
                6332 App

            C:\Users\admin>jstack -l 6332

                Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.121-b13 mixed mode):

                ...

                "main" #1 prio=5 os_prio=0 tid=0x00000000028b0800 nid=0x46c4 waiting on condition [0x00000000027cf000]
                   java.lang.Thread.State: TIMED_WAITING (sleeping)
                        at java.lang.Thread.sleep(Native Method)
                        at App.main(App.java:7)

                   Locked ownable synchronizers:
                        - None

                "VM Thread" os_prio=2 tid=0x000000001c2f9000 nid=0x5770 runnable

                "GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000002b16000 nid=0x4b0 runnable

                "GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000000002b17800 nid=0x556c runnable

                "GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000000002b19000 nid=0x3cf8 runnable

                "GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000000002b1b800 nid=0x5304 runnable

                "GC task thread#4 (ParallelGC)" os_prio=0 tid=0x0000000002b1d000 nid=0x4de4 runnable

                "GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000000002b1e000 nid=0x55c8 runnable

                "GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000000002b22000 nid=0x4f80 runnable

                "GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000000002b23800 nid=0x1c1c runnable

                "VM Periodic Task Thread" os_prio=2 tid=0x000000001e34c000 nid=0x2340 waiting on condition

                JNI global references: 6

        在 D:\demo 目录下创建 App2.java 文件,模拟 2 个线程死锁的场景,代码内容如下:

public class App2 {
                public static void main( String[] args ) {

                    System.out.println("App2 running ...");

                    Thread t1 = new Thread(new DeadLock(true), "Thread DeadLock 1");
                    Thread t2 = new Thread(new DeadLock(false), "Thread DealLock 2");
                    t1.start();
                    t2.start();
                }
            }

            class DeadLock implements Runnable{

                private static Object obj1 = new Object();
                private static Object obj2 = new Object();
                private boolean flag;

                public DeadLock(boolean flag){
                    this.flag = flag;
                }

                @Override
                public void run(){
                    String name = Thread.currentThread().getName();
                    System.out.println(name + ":running ...");

                    /*
                    * 同时开启线程1和线程2:
                    * 1. 如果让线程1执行 "代码1"(flag==true),让线程2执行 "代码2" (flag==false), 会死锁;
                    * 2. 如果让线程1执行 "代码2"(flag==false),让线程2执行 "代码1" (flag==true), 会死锁;
                    * 3. 如果让线程1执行 "代码1"(flag==true),让线程2执行 "代码1" (flag==true), 不会死锁;
                    * 4. 如果让线程1执行 "代码2"(flag==false),让线程2执行 "代码2" (flag==false), 不会死锁;
                    */
                    if (flag) {
                        // 代码1
                        synchronized(obj1){
                            System.out.println(name + ": lock obj1");
                            System.out.println(name + ": try to lock obj2 ...");
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            synchronized(obj2){
                                // 死锁时执行不到这里
                                System.out.println(name +": after 1 second, lock obj2");
                            }
                        }
                    } else {
                        // 代码2
                        synchronized(obj2){
                            System.out.println(name + ": lock obj2");
                            System.out.println(name + ": try to lock obj1 ...");
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            synchronized(obj1){
                                // 死锁时执行不到这里
                                System.out.println(name +": after 1 second, lock obj1");
                            }
                        }
                    }
                }
            }

        在命令行控制台下,命令行编译运行:

D:\demo>javac -encoding UTF-8 App2.java

            D:\demo>java App2

                App2 running ...
                Thread DeadLock 1:running ...
                Thread DealLock 2:running ...
                Thread DealLock 2: lock obj2
                Thread DealLock 2: try to lock obj1 ...
                Thread DeadLock 1: lock obj1
                Thread DeadLock 1: try to lock obj2 ...

        打开一个新的命令行控制台,运行如下命令:

C:\Users\admin>jps

                6512 App2
                14440 Jps
                
            C:\Users\admin>jstack -l 6512

                ...

                JNI global references: 6

                Found one Java-level deadlock:
                =============================
                "Thread DealLock 2":
                waiting to lock monitor 0x0000000002c3afc8 (object 0x000000076bc5f4e0, a java.lang.Object),
                which is held by "Thread DeadLock 1"
                "Thread DeadLock 1":
                waiting to lock monitor 0x0000000002c3da68 (object 0x000000076bc5f4f0, a java.lang.Object),
                which is held by "Thread DealLock 2"

                Java stack information for the threads listed above:
                ===================================================
                "Thread DealLock 2":
                        at DeadLock.run(App2.java:60)
                        - waiting to lock <0x000000076bc5f4e0> (a java.lang.Object)
                        - locked <0x000000076bc5f4f0> (a java.lang.Object)
                        at java.lang.Thread.run(Unknown Source)
                "Thread DeadLock 1":
                        at DeadLock.run(App2.java:45)
                        - waiting to lock <0x000000076bc5f4f0> (a java.lang.Object)
                        - locked <0x000000076bc5f4e0> (a java.lang.Object)
                        at java.lang.Thread.run(Unknown Source)

                Found 1 deadlock.

            C:\Users\admin>jstack -m -l 6512

                Attaching to process ID 6512, please wait...
                Debugger attached successfully.
                Server compiler detected.
                JVM version is 25.121-b13
                Deadlock Detection:

                Found one Java-level deadlock:
                =============================

                "Thread DeadLock 1":
                  waiting to lock Monitor@0x0000000002c3da68 (Object@0x000000076bc5f4f0, a java/lang/Object),
                  which is held by "Thread DealLock 2"
                "Thread DealLock 2":
                  waiting to lock Monitor@0x0000000002c3afc8 (Object@0x000000076bc5f4e0, a java/lang/Object),
                  which is held by "Thread DeadLock 1"

                Found a total of 1 deadlock.

                ...