JDK 自带的应用诊断相关的实用工具
工欲善其事, 必先利其器. 对于诊断 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应用很有帮助.
转载自:https://juejin.cn/post/7222845337492455484