在企业级 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.
...