本文为《深入学习 JVM 系列》第二十一篇文章
Jhsdb 是 JDK9 引入的新的命令行工具,它有 clhsdb、debugd、hsdb、jstack、jmap、jinfo、jsnap 这些 mode 可以使用,其中有几个在名称和功能上与以前的 JDK 发行版中可用的各个命令行工具相对应。看得出来,官方想要 jhsdb 工具整合多个其他工具的功能,甚至还做了一些功能拓展。所以本文将带大家认识一下 jhsdb 下的这些 mode。
在使用 jhsdb 工具之前,必须先获取 PID,所以我们先来认识一下 jps 命令。
jps
jps(JVM Process Status Tool)可以列出正在运行的虚拟机进程,需要注意的是,jps 仅查找当前用户的 Java 进程,而不是当前系统中的所有进程。
jps [options] [hostid] 复制代码
在默认情况下,jps 的输出信息包括 Java 进程的进程 ID 以及主类名。我们还可以通过追加参数,来打印额外的信息。
- -v:输出传递给 JVM 的参数(如-XX:+UnlockExperimentalVMOptions -XX:+UseZGC)
- -l: 打印模块名以及包名
- -m:将打印传递给主类的参数
或者根据进程名称查找准确的 id。
ps aux|grep xxxx 复制代码
需要注意的是,如果某 Java 进程关闭了默认开启的UsePerfData参数(即使用参数-XX:-UsePerfData),那么 jps 命令就无法探知该 Java 进程。
当获得 Java 进程的进程 ID 之后,我们便可以调用接下来介绍的各项监控及诊断工具了。
除了任何必需 jstack
、jmap
、jinfo
或jsnap
特定于模式的选项外,pid
、exe
、core
这三个选项适用于所有模式。
--pid
挂起进程的进程ID。--exe
可执行文件名。--core
核心转储文件名。
关于 --exe 和 --core 的使用,暂时没有测试成功。
jmap
jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。如果不想使用 jmap 命令,可以配置程序启动时的 JVM 参数,比如说 -XX:+HeapDumpOnOutOfMemoryError 参数,可以在程序发生内存溢出异常后自动生成 dump 文件。通过 -XX:+HeapDumpOnCtrlBreak 参数则可以使用[Ctrl]+[Break]键让虚拟机生成堆转储快照文件,又或者在 Linux 系统下通过 Kill-3 命令发送进程退出信号“恐吓”一下虚拟机,也能顺利拿到堆转储快照。
JDK 自带的 jmap 命令格式如下:
jmap [option] [pid] jmap -clstats <pid> to connect to running process and print class loader statistics jmap -finalizerinfo <pid> to connect to running process and print information on objects awaiting finalization jmap -histo[:live] <pid> to connect to running process and print histogram of java object heap if the "live" suboption is specified, only count live objects jmap -dump:<dump-options> <pid> to connect to running process and dump java heap dump-options: live dump only live objects; if not specified, all objects in the heap are dumped. format=b binary format file=<file> dump heap to <file> 复制代码
可以看到 jmap 已经没有了-heap 命令参数,我们来看一下 jhsdb jmap,其中 options 位置不一定非要在后面。
jhsdb jmap [--pid pid | --exe executable --core coredump] [options] // //其中 options 包括: <no option> to print same info as Solaris pmap --heap to print java heap summary //显示Java堆详细信息 --binaryheap to dump java heap in hprof binary format --dumpfile name of the dump file //导出 Java 虚拟机堆的快照,生成文件 --histo to print histogram of java object heap //打印 Java 对象堆的直方图 --clstats to print class loader statistics //打印 Java 堆的类加载器统计信息 --finalizerinfo to print information on objects awaiting finalization //打印有关等待完成的对象的信息 复制代码
(不能再使用 jmap -heap pid 的命令了,需要使用上面的命令)。使用旧的命令会报错:
Error: -heap option used Cannot connect to core dump or remote debug server. Use jhsdb jmap instead 复制代码
这里只演示两个简单示例,jmap -heap pid 展示 pid 的整体堆信息。
//JDK9 % jhsdb jmap --pid 2681 --heap Attaching to process ID 2681, please wait... Debugger attached successfully. Server compiler detected. JVM version is 9.0.4+11 using thread-local object allocation. Mark Sweep Compact GC Heap Configuration: MinHeapFreeRatio = 40 MaxHeapFreeRatio = 70 MaxHeapSize = 4294967296 (4096.0MB) NewSize = 10485760 (10.0MB) MaxNewSize = 10485760 (10.0MB) OldSize = 257949696 (246.0MB) 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 = 9437184 (9.0MB) used = 1718600 (1.6389846801757812MB) free = 7718584 (7.361015319824219MB) 18.210940890842014% used Eden Space: capacity = 8388608 (8.0MB) used = 1718600 (1.6389846801757812MB) free = 6670008 (6.361015319824219MB) 20.487308502197266% used From Space: capacity = 1048576 (1.0MB) used = 0 (0.0MB) free = 1048576 (1.0MB) 0.0% used To Space: capacity = 1048576 (1.0MB) used = 0 (0.0MB) free = 1048576 (1.0MB) 0.0% used tenured generation: capacity = 257949696 (246.0MB) used = 0 (0.0MB) free = 257949696 (246.0MB) 0.0% used 2851 interned Strings occupying 192840 bytes. 复制代码
这一命令相较于我们在 clhsdb 中执行的 universe 命令更加详细,可以看到详细的内存分配。
jmap -histo pid 展示 class 的内存情况
说明:instances(实例数)、bytes(大小)、classs name(类名)。它基本是按照使用使用大小逆序排列的。
% jhsdb jmap --pid 2681 --histo Attaching to process ID 2681, please wait... Debugger attached successfully. Server compiler detected. JVM version is 9.0.4+11 Iterating over heap. This may take a while... Object Histogram: num #instances #bytes Class description -------------------------------------------------------------------------- 1: 3885 205144 byte[] 2: 3497 111904 java.util.HashMap$Node 3: 1038 91248 java.lang.Object[] 4: 3393 81432 java.lang.String 5: 687 74920 java.util.HashMap$Node[] 6: 601 72984 java.lang.Class 7: 1376 44032 java.util.concurrent.ConcurrentHashMap$Node 8: 780 37440 java.util.HashMap 9: 24 35968 char[] //只输出前30的对象 % jhsdb jmap --histo --pid 2681 | head -n30 复制代码
关于该 jmap 的更多讲解,推荐阅读:java命令--jmap命令使用
关于 jmap 命令,使用最多的场景就是生成 dump 文件,其他命令的效果可有后续介绍到的监控工具来代替,比如说 VisualVM 等。下面是不同 JDK 环境导出 dump 文件的命令,不过需要注意的是,通过命令行方式执行时,JVM 是暂停服务的,所以对线上的运行会产生影响。不推荐该方式。
jhsdb jmap 命令也可以生成 dump 文件,下面两个命令都是可以的。
//JDK9 jhsdb jmap --binaryheap --dumpfile heap.hprof --pid 17714 jmap -dump:format=b,file=heap.hprof 17594 复制代码
在测试 JDK9 导出 dump 文件时遇到了个小问题,首先我是在 IDEA 中启动该程序,然后在命令行端口获取 pid,然后执行 jhsdb jmap 命令,结果报错了。
% jhsdb jmap --binaryheap --dumpfile heap.hprof --pid 17407 Attaching to process ID 17407, please wait... Debugger attached successfully. Server compiler detected. JVM version is 9.0.4+11 Exception in thread "main" sun.jvm.hotspot.types.WrongTypeException: No suitable match for type of address 0x000000032b938308 at jdk.hotspot.agent/sun.jvm.hotspot.runtime.InstanceConstructor.newWrongTypeException(InstanceConstructor.java:62) at jdk.hotspot.agent/sun.jvm.hotspot.runtime.VirtualBaseConstructor.instantiateWrapperFor(VirtualBaseConstructor.java:109) at jdk.hotspot.agent/sun.jvm.hotspot.oops.Metadata.instantiateWrapperFor(Metadata.java:73) at jdk.hotspot.agent/sun.jvm.hotspot.oops.Oop.getKlassForOopHandle(Oop.java:211) at jdk.hotspot.agent/sun.jvm.hotspot.oops.ObjectHeap.newOop(ObjectHeap.java:252) at jdk.hotspot.agent/sun.jvm.hotspot.oops.ObjectHeap.iterateLiveRegions(ObjectHeap.java:331) at jdk.hotspot.agent/sun.jvm.hotspot.oops.ObjectHeap.iterate(ObjectHeap.java:172) at jdk.hotspot.agent/sun.jvm.hotspot.utilities.AbstractHeapGraphWriter.write(AbstractHeapGraphWriter.java:51) at jdk.hotspot.agent/sun.jvm.hotspot.utilities.HeapHprofBinWriter.write(HeapHprofBinWriter.java:433) at jdk.hotspot.agent/sun.jvm.hotspot.tools.JMap.writeHeapHprofBin(JMap.java:182) at jdk.hotspot.agent/sun.jvm.hotspot.tools.JMap.run(JMap.java:97) at jdk.hotspot.agent/sun.jvm.hotspot.tools.Tool.startInternal(Tool.java:260) at jdk.hotspot.agent/sun.jvm.hotspot.tools.Tool.start(Tool.java:223) at jdk.hotspot.agent/sun.jvm.hotspot.tools.Tool.execute(Tool.java:118) at jdk.hotspot.agent/sun.jvm.hotspot.tools.JMap.main(JMap.java:176) at jdk.hotspot.agent/sun.jvm.hotspot.SALauncher.runJMAP(SALauncher.java:32 复制代码
关于错误原因,可以参考一下这篇文章,虽然错误不是完全一致,但是错误原因可以借鉴一下,初步认为是 IDEA 中程序运行时和命令行窗口中执行 jhsdb jmap 命令时,JVM 环境不一致。抱着试一试的态度,我在命令行窗口启动该程序,然后再次执行 jhsdb jmap 命令,成功生成 dump 文件。
% jhsdb jmap --binaryheap --dumpfile heap.hprof --pid 17714 Attaching to process ID 17714, please wait... Debugger attached successfully. Server compiler detected. JVM version is 9.0.4+11 heap written to heap.hprof 复制代码
jinfo
jinfo(Configuration Info for Java)的作用是实时查看和调整虚拟机各项参数。使用 jps 命令的-v参数可以查看虚拟机启动时显式指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,除了去找资料外,就只能使用 jinfo 的-flag 选项进行查询了。
Usage: jinfo <option> <pid> (to connect to a running process) where <option> is one of: -flag <name> to print the value of the named VM flag -flag [+|-]<name> to enable or disable the named VM flag -flag <name>=<value> to set the named VM flag to the given value -flags to print VM flags -sysprops to print Java system properties <no option> to print both VM flags and system properties -h | -help to print this help message 复制代码
比如可以使用 jinfo -flag +HeapDumpAfterFullGC 命令,开启所指定的 Java 进程的 HeapDumpAfterFullGC 参数。
再来看看 jhsdb jinfo 命令格式如下:
jhsdb jinfo [--pid pid | --exe executable --core coredump] [options] //其中 options 包括: --flags to print VM flags //打印 VM 标志。 --sysprops to print Java System properties //打印 Java 系统属性 <no option> to print both of the above //打印 VM 标志和 Java 系统属性 复制代码
而 --flags 只能输出进程配置的 JVM 参数
% jhsdb jinfo --pid 17714 --flags Attaching to process ID 17714, please wait... Debugger attached successfully. Server compiler detected. JVM version is 9.0.4+11 Non-default VM flags: -XX:CICompilerCount=4 -XX:ConcGCThreads=3 -XX:G1ConcRefinementThreads=10 -XX:G1HeapRegionSize=1048576 -XX:InitialHeapSize=268435456 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=2576351232 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5835340 -XX:NonProfiledCodeHeapSize=122911450 -XX:ProfiledCodeHeapSize=122911450 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:-UseAOT -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC Command line: 复制代码
jinfo --pid 用来查看目标 Java 进程的参数。
具体示例如下:
% jhsdb jinfo --pid 39050 复制代码
jsnap
jhsdb jsnap 打印性能计数器信息,格式如下
jhsdb jsnap [options] [--pid pid | --exe executable --core coredump] //其中 options 包括: --all to print all performance counters 复制代码
jsnap --pid 打印性能计数器信息。
% jhsdb jsnap --pid 3563 Attaching to process ID 3563, please wait... Debugger attached successfully. Server compiler detected. JVM version is 9.0.4+11 java.threads.started=5 event(s) java.threads.live=5 java.threads.livePeak=5 java.threads.daemon=4 java.cls.loadedClasses=519 event(s) java.cls.unloadedClasses=0 event(s) java.cls.sharedLoadedClasses=0 event(s) java.cls.sharedUnloadedClasses=0 event(s) java.ci.totalTime=26320291 tick(s) java.property.java.vm.specification.version=9 java.property.java.vm.specification.name=Java Virtual Machine Specification java.property.java.vm.specification.vendor=Oracle Corporation java.property.java.vm.version=9.0.4+11 java.property.java.vm.name=Java HotSpot(TM) 64-Bit Server VM java.property.java.vm.vendor=Oracle Corporation java.property.java.vm.info=mixed mode java.property.jdk.debug=release java.property.java.library.path=...... java.property.java.version=9.0.4 java.property.java.home=/Library/Java/JavaVirtualMachines/jdk-9.0.4.jdk/Contents/Home java.rt.vmFlags= java.rt.vmArgs=-XX:+UseSerialGC -Xmn10M -Xdebug -Xrunjdwp:transport=dt_socket,address=Ankangs-MacBook-Pro.local:55217,suspend=y 复制代码
jstack
jstack 打印目标 Java 进程中各个线程的栈轨迹,以及这些线程所持有的锁。
jhsdb jstack [--pid pid | --exe executable --core coredump] [options] // options包括: --locks to print java.util.concurrent locks --mixed to print both java and native frames (mixed mode) 复制代码
比如有这样一段死锁代码:
public class SyncDeadLock { private static Object objectA = new Object(); private static Object objectB = new Object(); public static void main(String[] args) { new SyncDeadLock().deadLock(); } private void deadLock() { Thread thread1 = new Thread(new Runnable() { @Override public void run() { synchronized (objectA) { try { System.out.println(Thread.currentThread().getName() + " get objectA ing!"); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " need objectB! Just waiting!"); synchronized (objectB) { System.out.println(Thread.currentThread().getName() + " get objectB ing!"); } } } }, "thread1"); Thread thread2 = new Thread(() -> { synchronized (objectB) { try { System.out.println(Thread.currentThread().getName() + " get objectB ing!"); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " need objectA! Just waiting!"); synchronized (objectA) { System.out.println(Thread.currentThread().getName() + " get objectA ing!"); } } }, "thread2"); thread1.start(); thread2.start(); } } 复制代码
执行上述代码,输出结果如下:
thread1 get objectA ing! thread2 get objectB ing! thread1 need objectB! Just waiting! thread2 need objectA! Just waiting! 复制代码
可以看出两个线程各拥有一个对象的锁,未释放锁之前,又想获取对方拥有的对象的锁,最终导致死锁。
接下来我们执行 jstack 命令来看一看死锁情况:
% jhsdb jstack --locks --pid 39186 Attaching to process ID 39186, please wait... Debugger attached successfully. Server compiler detected. JVM version is 9.0.4+11 Deadlock Detection: Found one Java-level deadlock: ============================= "thread1": waiting to lock Monitor@0x00007faf2b313f00 (Object@0x00000006cf535808, a java/lang/Object), which is held by "thread2" "thread2": waiting to lock Monitor@0x00007faf2b313d00 (Object@0x00000006cf5357f8, a java/lang/Object), which is held by "thread1" Found a total of 1 deadlock. Thread 27907: (state = BLOCKED) Locked ownable synchronizers: - None Thread 3587: (state = BLOCKED) Locked ownable synchronizers: - None Thread 36867: (state = BLOCKED) - com.msdn.java.commandLine.SyncDeadLock.lambda$deadLock$0() @bci=86, line=46 (Interpreted frame) - com.msdn.java.commandLine.SyncDeadLock$$Lambda$1.run() @bci=0 (Interpreted frame) - java.lang.Thread.run() @bci=11, line=844 (Interpreted frame) Locked ownable synchronizers: - None Thread 37123: (state = BLOCKED) - com.msdn.java.commandLine.SyncDeadLock$1.run() @bci=86, line=30 (Interpreted frame) - java.lang.Thread.run() @bci=11, line=844 (Interpreted frame) Locked ownable synchronizers: - None Thread 37891: (state = IN_NATIVE) - java.net.SocketInputStream.socketRead0(java.io.FileDescriptor, byte[], int, int, int) @bci=0 (Interpreted frame) - java.net.SocketInputStream.socketRead(java.io.FileDescriptor, byte[], int, int, int) @bci=8, line=116 (Interpreted frame) - java.net.SocketInputStream.read(byte[], int, int, int) @bci=117, line=171 (Interpreted frame) - java.net.SocketInputStream.read(byte[], int, int) @bci=11, line=141 (Interpreted frame) - sun.nio.cs.StreamDecoder.readBytes() @bci=135, line=284 (Interpreted frame) - sun.nio.cs.StreamDecoder.implRead(char[], int, int) @bci=112, line=326 (Interpreted frame) - sun.nio.cs.StreamDecoder.read(char[], int, int) @bci=180, line=178 (Interpreted frame) - java.io.InputStreamReader.read(char[], int, int) @bci=7, line=185 (Interpreted frame) - java.io.BufferedReader.fill() @bci=145, line=161 (Interpreted frame) - java.io.BufferedReader.readLine(boolean) @bci=44, line=326 (Interpreted frame) - java.io.BufferedReader.readLine() @bci=2, line=392 (Interpreted frame) - com.intellij.rt.execution.application.AppMainV2$1.run() @bci=36, line=61 (Interpreted frame) Locked ownable synchronizers: - None Thread 38403: (state = BLOCKED) - java.lang.Object.wait(long) @bci=0 (Interpreted frame) - java.lang.ref.ReferenceQueue.remove(long) @bci=59, line=151 (Interpreted frame) - jdk.internal.ref.CleanerImpl.run() @bci=65, line=148 (Interpreted frame) - java.lang.Thread.run() @bci=11, line=844 (Interpreted frame) - jdk.internal.misc.InnocuousThread.run() @bci=20, line=122 (Interpreted frame) Locked ownable synchronizers: - None Thread 40195: (state = BLOCKED) Locked ownable synchronizers: - None Thread 23299: (state = BLOCKED) - java.lang.Object.wait(long) @bci=0 (Interpreted frame) - java.lang.ref.ReferenceQueue.remove(long) @bci=59, line=151 (Interpreted frame) - java.lang.ref.ReferenceQueue.remove() @bci=2, line=172 (Interpreted frame) - java.lang.ref.Finalizer$FinalizerThread.run() @bci=37, line=216 (Interpreted frame) Locked ownable synchronizers: - None Thread 42243: (state = BLOCKED) - java.lang.ref.Reference.waitForReferencePendingList() @bci=0 (Interpreted frame) - java.lang.ref.Reference.processPendingReferences() @bci=0, line=174 (Interpreted frame) - java.lang.ref.Reference.access$000() @bci=0, line=44 (Interpreted frame) - java.lang.ref.Reference$ReferenceHandler.run() @bci=0, line=138 (Interpreted frame) Locked ownable synchronizers: - None 复制代码
我们可以看到,jstack不仅会打印线程的栈轨迹、线程状态(BLOCKED)、持有的锁(locked …)以及正在请求的锁(waiting to lock …),而且还会分析出具体的死锁。