likes
comments
collection
share

JDK 自带的应用诊断相关的实用工具

作者站长头像
站长
· 阅读数 3

工欲善其事, 必先利其器. 对于诊断 Java 应用来说, 我们有哪些有用的工具呢? 很多人可能很快想到网上有分析 Java verbose GC 日志的工具, 还有分析 Java 线程的工具, 有分析 Heap dump的工具, 有自动反编译Java 代码的工具, 还有多对 Java 应用做 profiling 的工具. 其实 JDK 本身自带了一些非常有用的工具, 这些工具本身都是 JDK 开发者自己觉得有用, 然后公开出来的, 所以, 我们先从这些最基本的工具看起, 能够帮助我们从各个侧面了解 Java 应用.

JDK 自带的工具都在 JDK 的 bin 目录, 比如下面就是 JDK 17 的 bin 目录所带的所有实用工具:

eric@supra jdk17 %  ls bin
jar		javadoc		jdb		jimage		jmod		jshell		keytool
jarsigner	javap		jdeprscan	jinfo		jpackage	jstack		rmiregistry
java		jcmd		jdeps		jlink		jps		jstat		serialver
javac		jconsole	jfr		jmap		jrunscript	jstatd

下面我们就介绍并实践一些有关诊断Java应用的命令, 有些在后面的实际操作中将会多次用到.

jps

首先我们要看的是这个这个最简单的命令, 它的作用很简单, 就是找出当前机器或者 container 里面的 Java 应用. 比如:

eric@supra ~ %  jps
86064 Jps
7985

简单的 jps 给出了当前机器上有2个正在运行的 Java 应用程序的进程id, 其中第一个是 Jps 命令本身, 因为它自己也是一个 Java 应用程序, 当它自己运行的时候, 它也会检测到它自己, 并且当你使用 Jps 的时候, 你永远会看到它自己. 另外一个进程号就是另外一个正在运行的 Java 应用程序.

所以, 它的功能就是找出你要找的Java 应用的进程号, 因为很多其它命令都是要知道 Java 进程号, 然后才能做进一步操足的. 这个找出进程号的功能很类似 Linux 平台上的 pgrep java 这个操作.

jinfo

jinfo 主要用来帮我们查看当前某个正在运行的 JVM 的 VM flags 和 系统属性. 比如我们想查看这运行的 Java 进程使用的JDK 版本, 或者它使用的系统文件路径分隔符, 或者这个VM enable 了哪些flags, jinfo 都能帮我们打印出来. 比如下面我们对某个 Java 进程使用 jinfo 命令的结果(由于篇幅所限, 省略了很多):

eric@supra ~ %  jinfo 7985
Java System Properties:
#Sun Feb 26 09:18:10 MST 2023
file.encoding=UTF-8
file.separator=/
java.class.path=/Applications/PyCharm CE.app/Contents/lib/junit4.jar\:/Applications/PyCharm CE.app/Contents/lib/platform-objectSerializer-annotations.jar\:/Applications/PyCharm CE.app/Contents/lib/rd.jar\:/Applications/PyCharm CE.app/Contents/lib/util_rt.jar
java.class.version=61.0
java.home=/Applications/PyCharm CE.app/Contents/jbr/Contents/Home
java.io.tmpdir=/var/folders/w7/bjbwyqmn56j8k1j6mqg4yys00000gq/T/
os.arch=x86_64
os.name=Mac OS X
os.version=13.2.1
path.separator=\:
user.country=CN
user.dir=/
user.timezone=America/Denver

VM Flags:
 -XX:ConcGCThreads=3 -XX:G1ConcRefinementThreads=13 -XX:G1EagerReclaimRemSetThreshold=8 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/xiatian/java_error_in_pycharm.hprof -XX:+IgnoreUnrecognizedVMOptions -XX:InitialHeapSize=134217728 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=2147483648 -XX:MaxNewSize=1287651328 -XX:MinHeapDeltaBytes=1048576 -XX:MinHeapSize=134217728 -XX:NonNMethodCodeHeapSize=5826188 -XX:NonProfiledCodeHeapSize=265522362 -XX:-OmitStackTraceInFastThrow -XX:ProfiledCodeHeapSize=265522362 -XX:ReservedCodeCacheSize=536870912 -XX:+SegmentedCodeCache -XX:SoftMaxHeapSize=2147483648 -XX:SoftRefLRUPolicyMSPerMB=50 -XX:SweeperThreshold=0.234375 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC -XX:-UseNUMA -XX:-UseNUMAInterleaving

VM Arguments:
jvm_args: -Xms128m -Xmx750m -XX:ReservedCodeCacheSize=512m -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=50 -XX:CICompilerCount=2 -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow -XX:+IgnoreUnrecognizedVMOptions 

java_command: <unknown>
java_class_path (initial): /Applications/PyCharm CE.app/Contents/lib/platform-objectSerializer-annotations.jar:/Applications/PyCharm CE.app/Contents/lib/rd.jar:/Applications/PyCharm CE.app/Contents/lib/util_rt.jar
Launcher Type: generic

除了查看 JVM 的flags 和系统属性之外, 它还可以设置某些 VM 的 flags. 其中某些是开关类的 flag, 都是可以通过 +/- 号来打开/关闭的. 另外一些设值的, 需要设置 name=value的形式设置. 比如:

eric@supra ~ %  jinfo -flag HeapDumpBeforeFullGC 7985
-XX:-HeapDumpBeforeFullGC
eric@supra ~ %  jinfo -flag +HeapDumpBeforeFullGC 7985
eric@supra ~ %  jinfo -flag HeapDumpBeforeFullGC 7985
-XX:+HeapDumpBeforeFullGC

上面的例子中, 对于7985这个进程来说, 我们先打印出 HeapDumpBeforeFullGC 这个 flag 的当前值, 这个 flag 表示的意义是: 在做 Full GC 之前, 产生一个heap dump, 它默认是关闭的(使用-表示), 然后, 我们使用 jinfo 给它设置打开开关, 然后再打印这个flag的新状态, 就看到它是打开的了.

一个设置值的例子:

eric@supra ~ % jinfo -flag MinHeapFreeRatio 7985
-XX:MinHeapFreeRatio=40
eric@supra ~ %  jinfo -flag MinHeapFreeRatio=35 7985
eric@supra ~ %  jinfo -flag MinHeapFreeRatio 7985
-XX:MinHeapFreeRatio=35

上面的例子中, 我们先输出了进程 7985 的 MinHeapFreeRatio 的当前值, 它当前值是40, 意思是当发生一个GC后, 最少要有40%的空闲比率, 才不会申请新的heap空间, 否则, 则回去扩展heap的大小. 第二条命令, 我们更改这个值到35%. 第三条命令显示我们改后的值.

下面是 jinfo 的命令说明, 可以看到, 它能更改 VM 的flag, 却不能更改JVM 的系统属性.

eric@supra ~ % jinfo --help
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 | -help to print this help message

那么是任何的 VM 的flag 我们都能更改吗? 其实并不是, 只有某些 flag 可以改, 另外一些不能改, 比如 heap 的总大小是一开始就确定的, 等程序运行起来, 只能查看这个值, 不能更改.

哪么哪些 flag 能在运行时候, 更改呢? 这个根据不同的 java 版本有少许不同, 不过对于特定版本的 java, 我们可以通过 java -XX:+PrintFlagsFinal --version 来查看所有的 flags, 其中标注有 manageable 的, 都是可以更改的. 比如下面是我 JDK 17 版本的上面可以更改的 flag 列表. 第一列是 flag 的类型, 第二列是 flag 的名字, 第三列是 flag 的默认值, 最后一列是 flag 的属性, 比如 manageable 表示可以改的, pd 表示平台相关的(platform dependence), default 表示有默认值.

 eric@supra ~ % java -XX:+PrintFlagsFinal --version | grep manage
    uintx G1PeriodicGCInterval                     = 0                                      {manageable} {default}
   double G1PeriodicGCSystemLoadThreshold          = 0.000000                               {manageable} {default}
     bool HeapDumpAfterFullGC                      = false                                  {manageable} {default}
     bool HeapDumpBeforeFullGC                     = false                                  {manageable} {default}
     intx HeapDumpGzipLevel                        = 0                                      {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}
     bool ShowCodeDetailsInExceptionMessages       = true                                   {manageable} {default}
   size_t SoftMaxHeapSize                          = 8589934592                             {manageable} {ergonomic}

jstat

jstat 命令输出系统的一些统计信息, 通过 jstat -options命令, 我们可以看到有哪些信息可以输出:

 eric@supra ~ % jstat -options
-class
-compiler
-gc
-gccapacity
-gccause
-gcmetacapacity
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcutil
-printcompilation

上面是 JDK 17 可以支持输出的一些统计, 可以看到主要是 GC 相关的一些信息, 还有编译器, 类和实时编译的一些统计信息.

使用起来也很简单, 我们只要知道当前Java进程号, 就可以了. 比如下面我们有个Java进程号是44537, 然后输出它的GC统计信息:

 eric@supra ~ % jstat -gc 44537
    S0C         S1C         S0U         S1U          EC           EU           OC           OU          MC         MU       CCSC      CCSU     YGC     YGCT     FGC    FGCT     CGC    CGCT       GCT
    17920.0     21504.0     17441.5         0.0     180224.0      55871.7     229888.0      23506.0    42456.0    40736.2    5376.0    4833.1      4     0.032     2     0.052     -         -     0.084

可以看到, 它输出了GC相关的各种信息, 各个列的解释是:

S0C, S1C, EC, OC, MC, CCSC: 分别是Survivor0, Survivor1, Eden, Old, Metaspace, Compressed class space 的 Capacity, 即总容量的大小.

S0U, S1U, EU, OU, MU, CCSU: 分别是Survivor0, Survivor1, Eden, Old, Metaspace, Compressed class space 的 Usage, 即已使用空间的大小.

YGC, YGCT, FGC, FGCT, CGC, CGCT, GCT: 分别是Young GC(年轻代GC)的次数和时间, Full GC的次数和时间, Concurrent GC的次数和时间, 以及所有的GC所用的时间.

下面是有关类的一些统计信息:

 eric@supra ~ % jstat -class  44537
Loaded  Bytes  Unloaded  Bytes     Time
  7749 15279.2        0     0.0       2.25

可以看到, JVM到当前共加载7749个类, 使用15279.2字节, 卸载0个类, 加载卸载类共花费2.25秒.

我们上面的例子中, 都只是输出了一行, jstat 可以持续的每隔固定的时间输出一次最新的统计信息, 比如下面的例子中, 我们让它每隔5s输出一次, 共输出6次.

 eric@supra ~ % jstat -gcutil  44537 5s 6
  S0     S1     E      O      M     CCS    YGC     YGCT     FGC    FGCT     CGC    CGCT       GCT
 97.33   0.00  31.00  10.22  95.95  89.90      4     0.032     2     0.052     -         -     0.084
 97.33   0.00  31.00  10.22  95.95  89.90      4     0.032     2     0.052     -         -     0.084
 97.33   0.00  31.00  10.22  95.95  89.90      4     0.032     2     0.052     -         -     0.084
 97.33   0.00  31.00  10.22  95.95  89.90      4     0.032     2     0.052     -         -     0.084
 97.33   0.00  31.00  10.22  95.95  89.90      4     0.032     2     0.052     -         -     0.084
 97.33   0.00  31.00  10.22  95.95  89.90      4     0.032     2     0.052     -         -     0.084

可以看到, 进程号后面可以添加参数: 每隔多久输出一次, 单位可以是s(秒)或ms(毫秒), 共输出多少次.

jstat 这些统计信息可以方便的告诉我们当前JVM内部的一些统计信息, 方便我们了解JVM内部当前的状态.

jcmd

JDK bin 目录下面这些命令当中, jcmd 是在诊断Java应用的过程中最有用的命令, 它不仅能查看当前JVM的启动参数, 系统属性, 还能产生 thread dump, heap dump, 启动停止JFR等.

下面, 我们就以实践的方式来看看有哪些比较有用的功能.

输出 JVM 的一些内部信息

eric@supra ~ % jcmd 44537 VM.uptime
44537:
47227.855 s

eric@supra ~ % jcmd 44537 VM.version
44537:
OpenJDK 64-Bit Server VM version 25.362-b09
JDK 8.0_362
 
eric@supra ~ % jcmd 44537 VM.command_line
44537:
VM Arguments:
java_command: /Users/eric/graph/lib/nugrap-console-current.jar
java_class_path (initial): /Users/eric/graph/lib/nugrap-console-current.jar
Launcher Type: SUN_STANDARD

eric@supra ~ % jcmd 44537 VM.system_properties
44537:
#Sun Apr 16 20:38:49 PDT 2023
java.runtime.name=OpenJDK Runtime Environment
         ... 省略 ....

eric@supra ~ % jcmd 44537 VM.flags
44537:
-XX:CICompilerCount=12 -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=8589934592 -XX:MaxNewSize=2863136768

上面的例子中, 我们输出了以 VM. 开头的一些子命令, 它告诉我们当前JVM的一些相关信息, 如: 启动多久了, JVM的版本, 命令行, 系统属性, 一些 flags.

输出 thread dump

我们可以通过 jstack 输出 thread dump, 也可以通过 jcmd <pid> Thread.print 来输出 thread dump.

eric@supra ~ % jcmd 44537 Thread.print
eric@supra ~ % jcmd 44537 Thread.print > /tmp/thread.txt

获得 heap dump

我们可以通过 jmap 来获得 heap dump, 也可以用 jcmd <pid> GC.heap_dump 来产生 heap dump:

eric@supra ~ % jcmd 44537 GC.heap_dump /tmp/heap.hprof
44537:
Heap dump file created

手动触发 JVM 做 Full GC

eric@supra ~ % jcmd 44537 GC.run
44537:
Command executed successfully

打印加载了哪些类且每个类有多少实例

eric@supra ~ % jcmd 44537 GC.class_histogram

打印 ClassLoader 相关信息

eric@supra ~ % jcmd 44537 VM.classloader_stats

打印类统计信息

GC.class_stats 能详细的展示每个类有多少实例, 占用多少内存, 并且在元数据(Metaspace)区占用了多少内存, 对于诊断 Metaspace的内存泄漏非常有用. 由于产生这些统计信息非常消耗系统资源, 对正常的应用影响较大, 尤其比较大的应用, 所以, 只有在应用启动时候, 添加 -XX:+UnlockDiagnosticVMOptions 启动选项, 才能执行这个命令.

eric@supra ~ % jcmd 44537 GC.class_stats
44537:
Index Super InstBytes KlassBytes annotations   CpAll MethodCount Bytecodes MethodAll   ROAll   RWAll    Total ClassName
    1    -1    711080        512           0       0           0         0         0      24     624      648 [B
    2    16    244424        680           0   22120         139      5679     41936   24632   42016    66648 java.lang.Class
    3    16    174960        624         128   14272         109      4576     50672   18640   48360    67000 java.lang.String
    4    16    126304        600           0    1368           9       213      2632    1488    3448     4936 java.util.concurrent.ConcurrentHashMap$Node
    5    -1    123080        512           0       0           0         0         0      24     624      648 [Ljava.lang.Object;
    6    16     95872        592           0    1392           7       149      1792    1152    2944     4096 java.util.HashMap$Node
    7    -1     67832        512           0       0           0         0         0      24     624      648 [C
    8    -1     47304        512           0       0           0         0         0      32     624      656 [Ljava.util.HashMap$Node;

查看 native 内存的使用情况

要查看 native 内存的使用, 必须在启动的时候添加启动参数 -XX:NativeMemoryTracking=summary-XX:NativeMemoryTracking=detail, 才能在启动后查看native 内存的使用情况, 这对于诊断非 heap 的内存泄漏很有帮助.

eric@supra ~ % jcmd 4823 VM.native_memory
4823:

Native Memory Tracking:

Total: reserved=10096545KB, committed=652509KB
-                 Java Heap (reserved=8388608KB, committed=524288KB)
                            (mmap: reserved=8388608KB, committed=524288KB)

-                     Class (reserved=1061162KB, committed=12970KB)
                            (classes #2009)
                            (  instance classes #1817, array classes #192)
                            (malloc=298KB #3511)
                            (mmap: reserved=1060864KB, committed=12672KB)
                            (  Metadata:   )
                            (    reserved=12288KB, committed=11264KB)
                            (    used=10912KB)
                            (    free=352KB)
                            (    waste=0KB =0.00%)
                            (  Class space:)
                            (    reserved=1048576KB, committed=1408KB)
                            (    used=1259KB)
                            (    free=149KB)
                            (    waste=0KB =0.00%)
              ... 省略 ...
-                    Thread (reserved=19540KB, committed=19540KB)
-                      Code (reserved=247907KB, committed=8223KB)
-                        GC (reserved=374483KB, committed=82643KB)
-                  Compiler (reserved=167KB, committed=167KB)
-                  Internal (reserved=625KB, committed=625KB)
-                     Other (reserved=10KB, committed=10KB)
-                    Symbol (reserved=3364KB, committed=3364KB)
-    Native Memory Tracking (reserved=344KB, committed=344KB)
-               Arena Chunk (reserved=200KB, committed=200KB)
-                   Logging (reserved=4KB, committed=4KB)
-                 Arguments (reserved=18KB, committed=18KB)
-                    Module (reserved=64KB, committed=64KB)
-              Synchronizer (reserved=41KB, committed=41KB)
-                 Safepoint (reserved=8KB, committed=8KB)

jcmd 还有一些其它子命令, 我们这里就不一一列举了, 通过 jcmd <pid> help 可以查看所有子命令.

总结

本文总结了日常的诊断过程中常用的一些 bin 目录里的小实用工具, 它们能帮我们很快的了解JVM和应用本身的一些情况, 熟悉这些命令的使用以及可以输出的内容, 对于我们诊断Java应用很有帮助.