目录
JVM监控和诊断工具
jps
它将打印所有正在运行的Java进程的相关信息。
在默认情况下,jps的输出信息包括Java进程的进程ID以及主类名。我们还可以通过追加参数,来打印额外的信息。例如,-l将打印模块名以及包名;-v将打印传递给Java虚拟机的参数(如-XX:+UnlockExperimentalVMOptions -XX:+UseZGC);-m将打印传递给主类的参数。
需要注意的是,如果某Java进程关闭了默认开启的UsePerfData参数(即使用参数-XX:-UsePerfData),那么jps命令(以及下面介绍的jstat)将无法探知该Java进程。
| $ jps -mlv | |
| 18331 org.example.Foo Hello World | |
| 18332 jdk.jcmd/sun.tools.jps.Jps -mlv -Dapplication.home=/Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home -Xms8m -Djdk.module.main=jdk.jcmd |
jstat
它可用来打印目标Java进程的性能数据。它包括多条子命令,如下所示:
| $ jstat -options | |
| -class | |
| -compiler | |
| -gc | |
| -gccapacity | |
| -gccause | |
| -gcmetacapacity | |
| -gcnew | |
| -gcnewcapacity | |
| -gcold | |
| -gcoldcapacity | |
| -gcutil | |
| -printcompilation |
在这些子命令中,-class将打印类加载相关的数据,-compiler和-printcompilation将打印即时编译相关的数据。剩下的都是以-gc为前缀的子命令,它们将打印垃圾回收相关的数据。
默认情况下,jstat只会打印一次性能数据。我们可以将它配置为每隔一段时间打印一次,直至目标Java进程终止,或者达到我们所配置的最大打印次数。具体示例如下所示:
| # Usage: jstat -outputOptions [-t] [-hlines] VMID [interval [count]] | |
| $ jstat -gc 22126 1s 4 | |
| S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT | |
| 17472,0 17472,0 0,0 0,0 139904,0 47146,4 349568,0 21321,0 30020,0 28001,8 4864,0 4673,4 22 0,080 3 0,270 0 0,000 0,350 | |
| 17472,0 17472,0 420,6 0,0 139904,0 11178,4 349568,0 21321,0 30020,0 28090,1 4864,0 4674,2 28 0,084 3 0,270 0 0,000 0,354 | |
| 17472,0 17472,0 0,0 403,9 139904,0 139538,4 349568,0 21323,4 30020,0 28137,2 4864,0 4674,2 34 0,088 4 0,359 0 0,000 0,446 | |
| 17472,0 17472,0 0,0 0,0 139904,0 0,0 349568,0 21326,1 30020,0 28093,6 4864,0 4673,4 38 0,091 5 0,445 0 0,000 0,536 |
在上面这个示例中,22126进程是一个使用了CMS垃圾回收器的Java进程。我们利用jstat的-gc子命令,来打印该进程垃圾回收相关的数据。命令最后的1s 4表示每隔1秒打印一次,共打印4次。在-gc子命令的输出中,前四列分别为两个Survivor区的容量(Capacity)和已使用量(Utility)。我们可以看到,这两个Survivor区的容量相等,而且始终有一个Survivor区的内存使用量为0。当使用默认的G1 GC时,输出结果则有另一些特征:
| $ jstat -gc 22208 1s | |
| S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT | |
| 0,0 16384,0 0,0 16384,0 210944,0 192512,0 133120,0 5332,5 28848,0 26886,4 4864,0 4620,5 19 0,067 1 0,016 2 0,002 0,084 | |
| 0,0 16384,0 0,0 16384,0 210944,0 83968,0 133120,0 5749,9 29104,0 27132,8 4864,0 4621,0 21 0,078 1 0,016 2 0,002 0,095 | |
| 0,0 0,0 0,0 0,0 71680,0 18432,0 45056,0 20285,1 29872,0 27952,4 4864,0 4671,6 23 0,089 2 0,063 2 0,002 0,153 | |
| 0,0 2048,0 0,0 2048,0 69632,0 28672,0 45056,0 18608,1 30128,0 28030,4 4864,0 4672,4 32 0,093 2 0,063 2 0,002 0,158 | |
| ... |
在上面这个示例中,jstat每隔1s便会打印垃圾回收的信息,并且不断重复下去。S0C和S0U始终为0,而且另一个Survivor区的容量(S1C)可能会下降至0。这是因为,当使用G1 GC时,Java虚拟机不再设置Eden区、Survivor区,老年代区的内存边界,而是将堆划分为若干个等长内存区域。每个内存区域都可以作为Eden区、Survivor区以及老年代区中的任一种,并且可以在不同区域类型之间来回切换。
换句话说,逻辑上只有一个Survivor区。当需要迁移Survivor区中的数据时(即Copying GC),只需另外申请一个或多个内存区域,作为新的Survivor区。因此,Java虚拟机决定在使用G1 GC时,将所有Survivor内存区域的总容量以及已使用量存放至S1C和S1U中,而S0C和S0U则被设置为0。
当发生垃圾回收时,Java虚拟机可能出现Survivor内存区域内的对象全被回收或晋升的现象。在这种情况下,Java虚拟机会将这块内存区域回收,并标记为可分配的状态。这样子做的结果是,堆中可能完全没有Survivor内存区域,因而相应的S1C和S1U将会是0。
jstat还有一个非常有用的参数-t,它将在每行数据之前打印目标Java进程的启动时间。例如,在下面这个示例中,第一列代表该Java进程已经启动了10.7秒。
| $ jstat -gc -t 22407 | |
| Timestamp S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT | |
| 10,7 0,0 0,0 0,0 0,0 55296,0 45056,0 34816,0 20267,8 30128,0 27975,3 4864,0 4671,6 33 0,086 3 0,111 2 0,001 0,198 |
表中CGC和CGCT,它们分别代表并发GC Stop-The-World的次数和时间。可以比较Java进程的启动时间以及总GC时间(GCT列),或者两次测量的间隔时间以及总GC时间的增量,来得出GC时间占运行时间的比例。如果该比例超过20%,则说明目前堆的压力较大;如果该比例超过90%,则说明堆里几乎没有可用空间,随时都可能抛出OOM异常。
jstat还可以用来判断是否出现内存泄漏。在长时间运行的Java程序中,可以运行jstat命令连续获取多行性能数据,并取这几行数据中OU列(即已占用的老年代内存)的最小值。然后,我们每隔一段较长的时间重复一次上述操作,来获得多组OU最小值。如果这些值呈上涨趋势,则说明该Java程序的老年代内存已使用量在不断上涨,这意味着无法回收的对象在不断增加,因此很有可能存在内存泄漏。
jmap
jmap同样包括多条子命令。
-clstats,该子命令将打印被加载类的信息。-finalizerinfo,该子命令将打印所有待finalize的对象。-histo,该子命令将统计各个类的实例数目以及占用内存,并按照内存使用量从多至少的顺序排列。此外,-histo:live只统计堆中的存活对象。-dump,该子命令将导出Java虚拟机堆的快照。同样,-dump:live只保存堆中的存活对象。
通常会利用jmap -dump:live,format=b,file=filename.bin命令,将堆中所有存活对象导出至一个文件之中。
这里format=b将使jmap导出与-XX:+HeapDumpAfterFullGC、-XX:+HeapDumpOnOutOfMemoryError格式一致的文件。这种格式的文件可以被其他GUI工具查看。
由于jmap将访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap需要借助安全点机制,让所有线程停留在不改变堆中数据的状态。也就是说,由jmap导出的堆快照必定是安全点位置的。这可能导致基于该堆快照的分析结果存在偏差。举个例子,假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么:live选项将无法探知到这些对象。
另外,如果某个线程长时间无法跑到安全点,jmap将一直等下去。而jstat不同。这是因为垃圾回收器会主动将jstat所需要的摘要数据保存至固定位置之中,而jstat只需直接读取即可。
jmap(以及接下来的jinfo、jstack和jcmd)依赖于Java虚拟机的Attach API,因此只能监控本地Java进程。
一旦开启Java虚拟机参数DisableAttachMechanism(即使用参数-XX:+DisableAttachMechanism),基于Attach API的命令将无法执行。反过来说,如果不想被其他进程监控,那么需要开启该参数。
jinfo
jinfo命令可用来查看目标Java进程的参数,如传递给Java虚拟机的-X(即输出中的jvm_args)、-XX参数(即输出中的VM Flags),以及可在Java层面通过System.getProperty获取的-D参数(即输出中的System Properties)。
jinfo还可以用来修改目标Java进程的“manageable”虚拟机参数。
举个例子,可以使用jinfo -flag +HeapDumpAfterFullGC <PID>命令,开启<PID>所指定的Java进程的HeapDumpAfterFullGC参数。
可以通过下述命令查看其他"manageable"虚拟机参数:
| $ java -XX:+PrintFlagsFinal -version | grep manageable | |
| intx CMSAbortablePrecleanWaitMillis = 100 {manageable} {default} | |
| intx CMSTriggerInterval = -1 {manageable} {default} | |
| intx CMSWaitDuration = 2000 {manageable} {default} | |
| bool HeapDumpAfterFullGC = false {manageable} {default} | |
| bool HeapDumpBeforeFullGC = false {manageable} {default} | |
| bool HeapDumpOnOutOfMemoryError = false {manageable} {default} | |
| ccstr HeapDumpPath = {manageable} {default} | |
| uintx MaxHeapFreeRatio = 70 {manageable} {default} | |
| uintx MinHeapFreeRatio = 40 {manageable} {default} | |
| bool PrintClassHistogram = false {manageable} {default} | |
| bool PrintConcurrentLocks = false {manageable} {default} | |
| java version "11" 2018-09-25 | |
| Java(TM) SE Runtime Environment 18.9 (build 11+28) | |
| Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11+28, mixed mode) |
jstack
jstack命令可以用来打印目标Java进程中各个线程的栈轨迹,以及这些线程所持有的锁。jstack的其中一个应用场景便是死锁检测。jstack不仅会打印线程的栈轨迹、线程状态(BLOCKED)、持有的锁(locked …)以及正在请求的锁(waiting to lock …),而且还会分析出具体的死锁。
总结
-
jps将打印所有正在运行的Java进程。 -
jstat允许用户查看目标Java进程的类加载、即时编译以及垃圾回收相关的信息。它常用于检测垃圾回收问题以及内存泄漏问题。 -
jmap允许用户统计目标Java进程的堆中存放的Java对象,并将它们导出成二进制文件。 -
jinfo将打印目标Java进程的配置参数,并能够改动其中manageabe的参数。 -
jstack将打印目标Java进程中各个线程的栈轨迹、线程状态、锁状况等信息。它还将自动检测死锁。