1. 概述
Java虚拟机(JVM)是使计算机能够运行Java程序的虚拟机,在本文中,我们将了解如何轻松诊断正在运行的JVM。
JDK本身就有很多工具可用于各种开发、监控和故障排除活动。我们来看一下jcmd,它非常易于使用,并且可以提供有关正在运行的JVM的各种信息。此外,从JDK 7开始,jcmd是推荐的工具,它可以增强JVM诊断功能,并且不会或最大限度地减少性能开销。
2. 什么是jcmd?
这是一个向正在运行的JVM发送诊断命令请求的实用程序,但是,它必须在运行JVM的同一台计算机上使用。更多详细信息,请参阅其文档。
让我们看看如何将此实用程序与服务器上运行的示例Java应用程序一起使用。
3. 如何使用jcmd?
让我们使用JDK 11上的Spring Initializr创建一个快速演示Web应用程序,现在,让我们启动服务器并使用jcmd对其进行诊断。
3.1 获取PID
我们知道每个进程都有一个关联的进程ID,称为PID。因此,为了获取应用程序的关联PID,我们可以使用jcmd,它将列出所有适用的Java进程,如下所示:
root@c6b47b129071:/# jcmd
65 jdk.jcmd/sun.tools.jcmd.JCmd
18 /home/pgm/demo-0.0.1-SNAPSHOT.jar
root@c6b47b129071:/#
在这里,我们可以看到正在运行的应用程序的PID是18。
3.2 获取可能的jcmd用途列表
首先让我们了解一下jcmd PID help命令可用的选项:
root@c6b47b129071:/# jcmd 18 help
18:
The following commands are available:
Compiler.CodeHeap_Analytics
Compiler.codecache
Compiler.codelist
Compiler.directives_add
Compiler.directives_clear
Compiler.directives_print
Compiler.directives_remove
Compiler.queue
GC.class_histogram
GC.class_stats
GC.finalizer_info
GC.heap_dump
GC.heap_info
GC.run
GC.run_finalization
JFR.check
JFR.configure
JFR.dump
JFR.start
JFR.stop
JVMTI.agent_load
JVMTI.data_dump
ManagementAgent.start
ManagementAgent.start_local
ManagementAgent.status
ManagementAgent.stop
Thread.print
VM.class_hierarchy
VM.classloader_stats
VM.classloaders
VM.command_line
VM.dynlibs
VM.flags
VM.info
VM.log
VM.metaspace
VM.native_memory
VM.print_touched_methods
VM.set_flag
VM.stringtable
VM.symboltable
VM.system_properties
VM.systemdictionary
VM.uptime
VM.version
help
不同版本的HotSpot VM可用的诊断命令可能不同。
4. jcmd命令
让我们探索一些最有用的jcmd命令选项来诊断我们正在运行的JVM。
4.1 VM.version
这是为了获取JVM基本详细信息,如下所示:
root@c6b47b129071:/# jcmd 18 VM.version
18:
OpenJDK 64-Bit Server VM version 11.0.11+9-Ubuntu-0ubuntu2.20.04
JDK 11.0.11
root@c6b47b129071:/#
在这里我们可以看到正在使用OpenJDK 11用于示例应用程序。
4.2 VM.system_properties
这将打印虚拟机的所有系统属性设置,可能会显示数百行信息:
root@c6b47b129071:/# jcmd 18 VM.system_properties
18:
#Thu Jul 22 10:56:13 IST 2021
awt.toolkit=sun.awt.X11.XToolkit
java.specification.version=11
sun.cpu.isalist=
sun.jnu.encoding=ANSI_X3.4-1968
java.class.path=/home/pgm/demo-0.0.1-SNAPSHOT.jar
java.vm.vendor=Ubuntu
sun.arch.data.model=64
catalina.useNaming=false
java.vendor.url=https\://ubuntu.com/
user.timezone=Asia/Kolkata
java.vm.specification.version=11
...
4.3 VM.flags
对于我们的示例应用程序,这将打印所有使用的VM参数,这些参数要么是我们指定的,要么是JVM默认使用的。在这里,我们可以注意到各种默认的VM参数,如下所示:
root@c6b47b129071:/# jcmd 18 VM.flags
18:
-XX:CICompilerCount=3 -XX:CompressedClassSpaceSize=260046848 -XX:ConcGCThreads=1 -XX:G1ConcRefinementThreads=4 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=536870912 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=536870912 -XX:MaxMetaspaceSize=268435456 -XX:MaxNewSize=321912832 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5830732 -XX:NonProfiledCodeHeapSize=122913754 -XX:ProfiledCodeHeapSize=122913754 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:ThreadStackSize=256 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC
root@c6b47b129071:/#
类似地,其他命令,如VM.command_line、VM.uptime、VM.dynlibs也提供了有关所使用的各种其他属性的其他基本和有用的详细信息。
以上所有命令主要是为了获取不同的JVM相关详细信息,现在,让我们再看看一些有助于排除JVM相关故障的命令。
4.4 Thread.print
此命令用于获取即时线程转储,因此,它将打印所有正在运行的线程的堆栈跟踪。以下是使用方法,根据正在使用的线程数量,输出可能会比较长:
root@c6b47b129071:/# jcmd 18 Thread.print
18:
2021-07-22 10:58:08
Full thread dump OpenJDK 64-Bit Server VM (11.0.11+9-Ubuntu-0ubuntu2.20.04 mixed mode, sharing):
Threads class SMR info:
_java_thread_list=0x00007f21cc0028d0, length=25, elements={
0x00007f2210244800, 0x00007f2210246800, 0x00007f221024b800, 0x00007f221024d800,
0x00007f221024f800, 0x00007f2210251800, 0x00007f2210253800, 0x00007f22102ae800,
0x00007f22114ef000, 0x00007f21a44ce000, 0x00007f22114e3800, 0x00007f221159d000,
0x00007f22113ce800, 0x00007f2210e78800, 0x00007f2210e7a000, 0x00007f2210f20800,
0x00007f2210f22800, 0x00007f2210f24800, 0x00007f2211065000, 0x00007f2211067000,
0x00007f2211069000, 0x00007f22110d7800, 0x00007f221122f800, 0x00007f2210016000,
0x00007f21cc001000
}
"Reference Handler" #2 daemon prio=10 os_prio=0 cpu=2.32ms elapsed=874.34s tid=0x00007f2210244800 nid=0x1a waiting on condition [0x00007f221452a000]
java.lang.Thread.State: RUNNABLE
at java.lang.ref.Reference.waitForReferencePendingList(java.base@11.0.11/Native Method)
at java.lang.ref.Reference.processPendingReferences(java.base@11.0.11/Reference.java:241)
at java.lang.ref.Reference$ReferenceHandler.run(java.base@11.0.11/Reference.java:213)
"Finalizer" #3 daemon prio=8 os_prio=0 cpu=0.32ms elapsed=874.34s tid=0x00007f2210246800 nid=0x1b in Object.wait() [0x00007f22144e9000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(java.base@11.0.11/Native Method)
- waiting on <0x00000000f7330898> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(java.base@11.0.11/ReferenceQueue.java:155)
- waiting to re-lock in wait() <0x00000000f7330898> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(java.base@11.0.11/ReferenceQueue.java:176)
at java.lang.ref.Finalizer$FinalizerThread.run(java.base@11.0.11/Finalizer.java:170)
"Signal Dispatcher" #4 daemon prio=9 os_prio=0 cpu=0.40ms elapsed=874.33s tid=0x00007f221024b800 nid=0x1c runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
有关使用其他选项捕获线程转储的详细讨论可以在这里找到。
4.5 GC.class_histogram
让我们使用另一个jcmd命令,它将提供有关堆使用情况的重要信息,此外,它还会列出所有具有多个实例的类(外部类或特定于应用程序的类)。同样,根据正在使用的类的数量,列表可能会有数百行:
root@c6b47b129071:/# jcmd 18 GC.class_histogram
18:
num #instances #bytes class name (module)
-------------------------------------------------------
1: 41457 2466648 [B (java.base@11.0.11)
2: 38656 927744 java.lang.String (java.base@11.0.11)
3: 6489 769520 java.lang.Class (java.base@11.0.11)
4: 21497 687904 java.util.concurrent.ConcurrentHashMap$Node (java.base@11.0.11)
5: 6570 578160 java.lang.reflect.Method (java.base@11.0.11)
6: 6384 360688 [Ljava.lang.Object; (java.base@11.0.11)
7: 9668 309376 java.util.HashMap$Node (java.base@11.0.11)
8: 7101 284040 java.util.LinkedHashMap$Entry (java.base@11.0.11)
9: 3033 283008 [Ljava.util.HashMap$Node; (java.base@11.0.11)
10: 2919 257000 [I (java.base@11.0.11)
11: 212 236096 [Ljava.util.concurrent.ConcurrentHashMap$Node; (java.base@11.0.11)
但是,如果这还不能提供清晰的信息,我们可以获取堆转储,接下来我们来看一下。
4.6 GC.heap_dump
此命令将立即提供JVM堆转储,因此,我们可以将堆转储提取到文件中,以便稍后进行分析,如下所示:
root@c6b47b129071:/# jcmd 18 GC.heap_dump ./demo_heap_dump
18:
Heap dump file created
root@c6b47b129071:/#
这里,demo_heap_dump是堆转储文件名。此外,它将创建在应用程序jar所在的同一位置。
4.7 JFR命令选项
在我们之前的文章中,我们讨论了使用JFR和JMC进行Java应用程序监控。现在,让我们研究一下可以用来分析应用程序性能问题的jcmd命令。
JFR(Java Flight Recorder)是JDK内置的分析和事件收集框架,JFR使我们能够收集有关JVM和Java应用程序行为的详细底层信息。此外,我们还可以使用JMC来可视化JFR收集的数据。因此,JFR和JMC共同构成了一个完整的工具链,用于持续收集底层和详细的运行时信息。
虽然如何使用JMC不在本文讨论范围内,但我们将了解如何使用jcmd创建JFR文件。JFR是一项商业功能,因此,默认情况下,它是禁用的。但是,可以使用“jcmd PID VM.unlock_commercial_features”启用它。
但是,我们在本文中使用的是OpenJDK,因此,我们已启用JFR。现在,让我们使用jcmd命令生成一个JFR文件,如下所示:
root@c6b47b129071:/# jcmd 18 JFR.start name=demo_recording settings=profile delay=10s duration=20s filename=./demorecording.jfr
18:
Recording 1 scheduled to start in 10 s. The result will be written to:
/demorecording.jfr
root@c6b47b129071:/# jcmd 18 JFR.check
18:
Recording 1: name=demo_recording duration=20s (delayed)
root@c6b47b129071:/# jcmd 18 JFR.check
18:
Recording 1: name=demo_recording duration=20s (running)
root@c6b47b129071:/# jcmd 18 JFR.check
18:
Recording 1: name=demo_recording duration=20s (stopped)
我们在jar应用程序所在的位置创建了一个名为demorecording.jfr的示例JFR录制文件,此外,此录制时长为20秒,并根据需求进行了配置。
此外,我们可以使用JFR.check命令检查JFR录制的状态。此外,我们可以使用JFR.stop命令立即停止并丢弃录制。另一方面,我们可以使用JFR.dump命令立即停止并转储录制。
4.8 VM.native_memory
这是最好的命令之一,它可以提供有关JVM上堆和非堆内存的大量有用详细信息,因此,它可以用于调整内存使用情况并检测任何内存泄漏。众所周知,JVM内存大致可以分为堆内存和非堆内存,为了获取完整的JVM内存使用情况详细信息,我们可以使用此实用程序。此外,它还有助于定义基于容器的应用程序的内存大小。
要使用此功能,我们需要重启应用程序并添加额外的虚拟机参数,例如XX:NativeMemoryTracking=summary或-XX:NativeMemoryTracking=detail。请注意,启用NMT会导致5%-10%的性能开销。
这将为我们提供一个新的PID来诊断:
root@c6b47b129071:/# jcmd 19 VM.native_memory
19:
Native Memory Tracking:
Total: reserved=1159598KB, committed=657786KB
- Java Heap (reserved=524288KB, committed=524288KB)
(mmap: reserved=524288KB, committed=524288KB)
- Class (reserved=279652KB, committed=29460KB)
(classes #6425)
( instance classes #5960, array classes #465)
(malloc=1124KB #15883)
(mmap: reserved=278528KB, committed=28336KB)
( Metadata: )
( reserved=24576KB, committed=24496KB)
( used=23824KB)
( free=672KB)
( waste=0KB =0.00%)
( Class space:)
( reserved=253952KB, committed=3840KB)
( used=3370KB)
( free=470KB)
( waste=0KB =0.00%)
- Thread (reserved=18439KB, committed=2699KB)
(thread #35)
(stack: reserved=18276KB, committed=2536KB)
(malloc=123KB #212)
(arena=39KB #68)
- Code (reserved=248370KB, committed=12490KB)
(malloc=682KB #3839)
(mmap: reserved=247688KB, committed=11808KB)
- GC (reserved=62483KB, committed=62483KB)
(malloc=10187KB #7071)
(mmap: reserved=52296KB, committed=52296KB)
- Compiler (reserved=146KB, committed=146KB)
(malloc=13KB #307)
(arena=133KB #5)
- Internal (reserved=460KB, committed=460KB)
(malloc=428KB #1421)
(mmap: reserved=32KB, committed=32KB)
- Other (reserved=16KB, committed=16KB)
(malloc=16KB #3)
- Symbol (reserved=6593KB, committed=6593KB)
(malloc=6042KB #72520)
(arena=552KB #1)
- Native Memory Tracking (reserved=1646KB, committed=1646KB)
(malloc=9KB #113)
(tracking overhead=1637KB)
- Shared class space (reserved=17036KB, committed=17036KB)
(mmap: reserved=17036KB, committed=17036KB)
- Arena Chunk (reserved=185KB, committed=185KB)
(malloc=185KB)
- Logging (reserved=4KB, committed=4KB)
(malloc=4KB #191)
- Arguments (reserved=18KB, committed=18KB)
(malloc=18KB #489)
- Module (reserved=124KB, committed=124KB)
(malloc=124KB #1521)
- Synchronizer (reserved=129KB, committed=129KB)
(malloc=129KB #1089)
- Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB)
在这里,除了Java Heap Memory之外,我们还可以注意到有关不同内存类型的细节,Class定义了用于存储类元数据的JVM内存。类似地,Thread定义了我们的应用程序线程正在使用的内存。Code提供了用于存储JIT生成代码的内存,Compiler本身占用了一些空间,GC也占用了一些空间。
此外,reserved可以估算出应用程序所需的内存,而commit则显示了已分配的最小内存。
5. 诊断内存泄漏
让我们看看如何识别JVM中是否存在内存泄漏;因此,首先我们需要有一个基准,然后需要监控一段时间,以了解上述任何内存类型的内存是否存在持续增长。
我们首先来了解一下JVM内存使用情况,如下所示:
root@c6b47b129071:/# jcmd 19 VM.native_memory baseline
19:
Baseline succeeded
现在,请将该应用程序正常或频繁使用一段时间。最后,使用diff来识别自基线以来的变化,如下所示:
root@c6b47b129071:/# jcmd 19 VM.native_memory summary.diff
19:
Native Memory Tracking:
Total: reserved=1162150KB +2540KB, committed=660930KB +3068KB
- Java Heap (reserved=524288KB, committed=524288KB)
(mmap: reserved=524288KB, committed=524288KB)
- Class (reserved=281737KB +2085KB, committed=31801KB +2341KB)
(classes #6821 +395)
( instance classes #6315 +355, array classes #506 +40)
(malloc=1161KB +37KB #16648 +750)
(mmap: reserved=280576KB +2048KB, committed=30640KB +2304KB)
( Metadata: )
( reserved=26624KB +2048KB, committed=26544KB +2048KB)
( used=25790KB +1947KB)
( free=754KB +101KB)
( waste=0KB =0.00%)
( Class space:)
( reserved=253952KB, committed=4096KB +256KB)
( used=3615KB +245KB)
( free=481KB +11KB)
( waste=0KB =0.00%)
- Thread (reserved=18439KB, committed=2779KB +80KB)
(thread #35)
(stack: reserved=18276KB, committed=2616KB +80KB)
(malloc=123KB #212)
(arena=39KB #68)
- Code (reserved=248396KB +21KB, committed=12772KB +213KB)
(malloc=708KB +21KB #3979 +110)
(mmap: reserved=247688KB, committed=12064KB +192KB)
- GC (reserved=62501KB +16KB, committed=62501KB +16KB)
(malloc=10205KB +16KB #7256 +146)
(mmap: reserved=52296KB, committed=52296KB)
- Compiler (reserved=161KB +15KB, committed=161KB +15KB)
(malloc=29KB +15KB #341 +34)
(arena=133KB #5)
- Internal (reserved=495KB +35KB, committed=495KB +35KB)
(malloc=463KB +35KB #1429 +8)
(mmap: reserved=32KB, committed=32KB)
- Other (reserved=52KB +36KB, committed=52KB +36KB)
(malloc=52KB +36KB #9 +6)
- Symbol (reserved=6846KB +252KB, committed=6846KB +252KB)
(malloc=6294KB +252KB #76359 +3839)
(arena=552KB #1)
- Native Memory Tracking (reserved=1727KB +77KB, committed=1727KB +77KB)
(malloc=11KB #150 +2)
(tracking overhead=1716KB +77KB)
- Shared class space (reserved=17036KB, committed=17036KB)
(mmap: reserved=17036KB, committed=17036KB)
- Arena Chunk (reserved=186KB, committed=186KB)
(malloc=186KB)
- Logging (reserved=4KB, committed=4KB)
(malloc=4KB #191)
- Arguments (reserved=18KB, committed=18KB)
(malloc=18KB #489)
- Module (reserved=124KB, committed=124KB)
(malloc=124KB #1528 +7)
- Synchronizer (reserved=132KB +3KB, committed=132KB +3KB)
(malloc=132KB +3KB #1111 +22)
- Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB)
随着GC运行时间的推移,我们会注意到内存使用量时增时减。但是,如果内存使用量出现不受控制的增长,则可能存在内存泄漏问题。因此,我们可以从这些统计数据中识别内存泄漏区域,例如Heap、Thread、Code、Class等。如果我们的应用程序需要更多内存,我们可以分别调整相应的虚拟机参数。
如果内存泄漏发生在堆(Heap)中,我们可以进行堆转储(如前所述)或者仅仅调整Xmx。同样,如果内存泄漏发生在线程(Thread)中,我们可以查找未处理的递归指令或调整Xss。
6. 总结
在本文中,我们介绍了一种用于诊断不同场景的JVM的实用程序。
我们还介绍了jcmd命令及其各种用法,用于获取堆转储、线程转储和JFR记录,以进行各种性能相关的分析。最后,我们还研究了一种使用jcmd诊断内存泄漏的方法。
Post Directory
