定位系统问题的时候,需要依据系统输出的数据信息查看具体原因,当然需要借助相应的工具以方便处理数据信息。
这里的数据信息包括:运行日志、GC 日志、异常堆栈、线程快照(threaddump/javacore 文件)、堆转储快照(heapdump/hprof文件)等。
JDK 的bin目录中提供了很多命令行工具,如我们熟悉的“java.exe”、“javac.exe”,其他命令参考bin目录。这些工具基本上是 lib/tools.jar 类库的再封装,主要功能代码在 tools 类库中实现。
这些监控工具,尤其是对已部署在生产环境上的应用程序排错是非常有帮助的:当应用程序部署到生产环境后,无论是能够直接接触服务器还是远程到服务器上都可能会受到限制,借助 JDK 监控工具,我们可以实现强大的监控分析功能。
下面主要介绍6种 JDK 监控和故障处理工具:
(1) jps:JVM Process Status Tool,显示指定系统内所有的HotSpot 虚拟机进程
(2) jinfo:Configuration Info for Java,显示虚拟机配置信息
(3)jmap:Memory Map for java,生成虚拟机内存转储快照(heapdump 文件)
(4) jstack:Stack Trace for Java,显示虚拟机的线程快照,定位线程出现长时间停顿的原因(死锁、死循环等)
(5)jstat:JVM Statistics Monitoring Tool,收集 HotSpot VM 各方面运行数据
(6) HSDIS: JIT 生成代码反汇编
1.1 jps
JVM Process Status Tool,显示指定系统内所有的HotSpot 虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称及这些进程的本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier)。
命令格式为:jps [options] [hostid]
可选参数如下:
- -q 指定jps只输出进程ID,不输出类的短名称
- -m 输出传递给java进程(主函数)的参数
- -l 输出主函数的完整路径
- -v 显示传递给JVM的参数
C:\Users\admin>jps -l
8896
1252 org.jetbrains.idea.maven.server.RemoteMavenServer
9460 sun.tools.jps.Jps
5324 org.jetbrains.jps.cmdline.Launcher
前面的 ID 为进程 ID,后面为主类的完整路径。
jps 可以通过RMI协议查询开启了 RMI 服务的远程虚拟机进程状态,参数 hostid 即 RMI 注册表中注册的主机名。
1.2 jinfo
jinfo(Configuration Info for Java) 可以实时查看和调整虚拟机各项参数。jps 命令的 -v 参数可以查看虚拟机启动时显式指定的参数列表,但若想知道未显式指定的参数的系统默认值,可以使用 jinfo 的 -flag 选项进行查询。
命令格式为:jinfo [options] pid
可选参数如下:
- -flag
打印指定JVM的参数值 - -flags
打印JVM的参数 - -flag [+|-]
启用/禁用指定的虚拟机参数 - -flag =
设置指定的参数值 - -sysprops
打印出 Java 系统属性,功能类似 System.getProperties();
C:\Users\admin>jinfo -flag CMSInitiatingOccupancyFraction 5324
-XX:CMSInitiatin
1.3 jmap
jmap(Memory Map for Java) 用于生成堆转储快照(heapdump或dump 文件)。该命令还可以查询 finalize 执行队列、Java 堆和永久代的详细信息,如空间使用率、当前所用的收集器信息等。
命令格式为:jmap [options] vmid
可选参数如下:
- -dump
生成 Java 堆转储快照,格式为: -dump:[live, ]format=b, file=, live 参数表示是否只dump存活对象,file 参数值要加上完整路径及导出的文件名称,示例如下:
jmap -dump:format=b,file=d:\OOMTestHeap.hprof 9028(进程号) - -finalizerinfo
显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象。(只在 linux 平台有效) - -heap
显示堆详细信息,如回收器、参数配置、分代信息等(只在 linux 平台有效) - -histo
显示堆中对象统计信息,包括类、实例数量、合计容量
如果信息过长,命令控制台不能显示靠前的信息,可将信息保存至磁盘,如:
C:\Users\admin>jmap -histo pid > e:111.txt
- -F
当使用 -dump 没有响应时,可使用该选项强制生成 dump 快照(只在 linux 平台有效)
1.4 jstack
jstack (Stack Trace for Java) 命令用于生成虚拟机当前时刻的线程快照(threaddump/javacore 文件),线程快照是指当前虚拟机内每条线程正在执行的方法堆栈的集合。
线程快照主要用于定位线程长时间停顿的原因,如线程死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。
命令格式为:jstack [option] vmid
可选参数如下:
- -F
当正常输出的请求不被响应时,强制输出线程堆栈 - -l
打印关于锁的信息 - -m
如果调用到本地方法的话,显示 C/C++ 的堆栈信息
对一个线程死锁的进程 jstack 如下:
C:\Users\admin>jps -l
7604
8168 sun.tools.jps.Jps
8952 org.jetbrains.jps.cmdline.Launcher
9852 com.java.jvm.test.DeadLockTest
jstack 分析进程9852(DeadLockTest)
C:\Users\admin>jstack -l 9852
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.162-b12 mixed mode):
"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000000ece800 nid=0x2724 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Thread-1" #12 prio=5 os_prio=0 tid=0x0000000018c38000 nid=0x2744 waiting for monitor entry [0x00000000198ef000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.java.jvm.test.DeadLockTest.run(DeadLockTest.java:73)
- waiting to lock <0x00000000d65a3080> (a java.lang.Object)
- locked <0x00000000d65a3090> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
"Thread-0" #11 prio=5 os_prio=0 tid=0x0000000018c35000 nid=0x2784 waiting for monitor entry [0x00000000197ef000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.java.jvm.test.DeadLockTest.run(DeadLockTest.java:60)
- waiting to lock <0x00000000d65a3090> (a java.lang.Object)
- locked <0x00000000d65a3080> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
...
"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x0000000018b35000 nid=0x2708 runnable [0x00000000191ee000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x00000000d662c178> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x00000000d662c178> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)
Locked ownable synchronizers:
- None
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x00000000177de800 nid=0x2430 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x0000000018af8800 nid=0x152c runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000000017768800 nid=0x26a8 in Object.wait() [0x0000000018aef000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d6408ec0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
- locked <0x00000000d6408ec0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:212)
Locked ownable synchronizers:
- None
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000002cf4000 nid=0x2594 in Object.wait() [0x00000000189ef000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d6406b68> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x00000000d6406b68> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
Locked ownable synchronizers:
- None
"VM Thread" os_prio=2 tid=0x0000000017747000 nid=0x26ac runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000002c18800 nid=0x25a8 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000000002c1a000 nid=0x26fc runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000000002c1b800 nid=0x2748 runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000000002c1d000 nid=0x2754 runnable
"VM Periodic Task Thread" os_prio=2 tid=0x0000000018c2d000 nid=0x26e4 waiting on condition
JNI global references: 12
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000002cfb788 (object 0x00000000d65a3080, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000000002cfcc28 (object 0x00000000d65a3090, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.java.jvm.test.DeadLockTest.run(DeadLockTest.java:73)
- waiting to lock <0x00000000d65a3080> (a java.lang.Object)
- locked <0x00000000d65a3090> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at com.java.jvm.test.DeadLockTest.run(DeadLockTest.java:60)
- waiting to lock <0x00000000d65a3090> (a java.lang.Object)
- locked <0x00000000d65a3080> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
从最后打印的信息可以分析出:
Thread-1 在等待由Thread-0持有的对象锁(lock <0x00000000d65a3080> );
Thread-0 在等待由Thread-1持有的对象锁(lock <0x00000000d65a3090> );
从而导致死锁问题。
此外,在JDK 1.5中Thread 类增加了一个 getAllStackTraces()方法用于获取虚拟机中所有线程的 StackTraceElement 对象,可以完成 jstack 大部分功能,在实际项目中可以使用该方法做个管理线程 stack 的页面,方便查看线程堆栈情况。
1.5 jstat
jstat(JVM Statistics-Monitoring Tool) 用于监视虚拟机各种运行状态信息,该命令可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据。
命令格式为:jstat [options] vmid [interval [s|ms] [count] ]
注:如果为本地虚拟机进程,VMID 和 LVMID相同,如果为远程虚拟机进程,VMID 的格式为:
[protocol:][//lvmid[@hostname[:port]/servername]
interval 表示查询间隔,count 表示查询次数,默认只查询一次。
如没500ms 查询进程 9852 垃圾收集情况,查10次,则命令如下:
jstat -gc 9852 500 10
option 此处表示虚拟机指定信息,主要包括类装载、垃圾收集、运行期编译状况3类,具体参数如下:
- -class
监视类装载、卸载、总空间及类装载所耗费的时间 - -gc
监视堆状况,各区、代容量,已用空间,GC等信息 - -gccapacity
主要关注堆各区使用到的最大、最小空间 - -gcutil
主要关注已用空间占总空间的百分比 - -gccause
与gcutil 功能一样,额外输出导致上一次GC产生的原因 - -gcnew
监视新生代GC - -gcnewcapacity
主要关注堆使用到的最大、最小空间 - -gcold
监视老年代GC - -gcoldcapacity
主要关注堆使用到的最大、最小空间 - -compiler
输出 JIT 编译器编译过的方法、耗时信息
输出的各列描述如下表:
显示列名 | 描述 |
S0C | 年轻代中第一个survivor(幸存区)的容量 (KB) |
S1C | 年轻代中第二个survivor(幸存区)的容量 (KB) |
S0U | 年轻代中第一个survivor(幸存区)目前已使用空间 (KB) |
S1U | 年轻代中第二个survivor(幸存区)目前已使用空间 (KB) |
EC | 年轻代中Eden(伊甸园)的容量 (KB) |
EU | 年轻代中Eden(伊甸园)目前已使用空间 (KB) |
OC | Old代的容量 (KB) |
OU | Old代目前已使用空间 (KB) |
PC | Perm(持久代)的容量 (KB) |
PU | Perm(持久代)目前已使用空间 (KB) |
YGC | 从应用程序启动到采样时年轻代中gc次数 |
YGCT | 从应用程序启动到采样时年轻代中gc所用时间(s) |
FGC | 从应用程序启动到采样时old代(全gc)gc次数 |
FGCT | 从应用程序启动到采样时old代(全gc)gc所用时间(s) |
GCT | 从应用程序启动到采样时gc用的总时间(s) |
S0 | 年轻代中第一个survivor(幸存区)已使用的占当前容量百分比 |
S1 | 年轻代中第二个survivor(幸存区)已使用的占当前容量百分比 |
E | 年轻代中Eden(伊甸园)已使用的占当前容量百分比 |
O | old代已使用的占当前容量百分比 |
P | perm代已使用的占当前容量百分比 |
YGC | 从应用程序启动到采样时年轻代中gc次数 |
YGCT | 从应用程序启动到采样时年轻代中gc所用时间(s) |
FGC | 从应用程序启动到采样时old代(全gc)gc次数 |
FGCT | 从应用程序启动到采样时old代(全gc)gc所用时间(s) |
GCT | 从应用程序启动到采样时gc用的总时间(s) |
1.6 HSDIS: JIT 生成代码反汇编
分析程序如何执行,可以使用软件调试工具(GDB、Windbg等)来断点调试,但在JVM中会比较麻烦,因为执行代码是通过 JIT 编译器动态生成到 CodeBuffer 中的,而 HSDIS 插件正好可以解决这个问题。
HSDIS 是 HotSpot 虚拟机 JIT 编译代码的反汇编插件,包含在虚拟机源码中。其作用是让虚拟机的 -XX:+PrintAssembly 指令调用它来把动态生成的本地代码还原为汇编代码输出(包括注释),进而分析问题。
注: 如果 HotSpot 为Debug 版的,可以直接通过 -XX:+PrintAssembly 指令使用插件;如果是 Product 版的,需加上 -XX:+UnlockDiagnosticVMOption 参数。
命令格式为:
java -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*类名.方法名 -XX:CompileCommand=compileonly, *类名.方法名 包名.类名
参数说明如下:
- –XX:+PrintAssembly
输出反汇编内容 - -Xcomp
使虚拟机以编译模式执行代码 - -XX:CompileCommand
使虚拟机不内联(don’t inline)方法,并且只编译该(compileonly)方法 - -其中类名为java文件编译后的class类名
实例如下:
package test;
public class Test {
int a = 1;
int b = 2;
public int sum() {
return a+b;
}
public static void main(String[] args){
Test test = new Test();
test.sum();
}
}
javac编译java文件后,命令窗口进入文件目录,反汇编命令为:
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*Test.sum -XX:CompileCommand=compileonly,*Test.sum Test
参考资料:
深入理解Java虚拟机 JVM高级特性与最佳实践