1. 概述

Java虚拟机(JVM)是一种使计算机能够运行Java程序的虚拟机。 在本文中,我们将了解如何轻松诊断正在运行的 JVM。

JDK 本身提供了许多工具,可用于各种开发、监控和故障排除活动。让我们看一下 jcmd ,它非常易于使用,并且可以提供有关正在运行的 JVM 的各种信息。 此外,从 JDK 7 开始, jcmd 是推荐的工具,用于增强 JVM 诊断,而不会产生或最小化性能开销。

2. jcmd 是什么?

这是一个向正在运行的 JVM 发送诊断命令请求的实用程序。 但是,它必须在运行 JVM 的同一台机器上使用。 其他详细信息可在其文档中找到。

让我们看看如何将此实用程序与在服务器上运行的示例 Java 应用程序一起使用。

3.如何使用 jcmd

让我们使用 Spring InitializrJDK11 创建一个快速演示 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 帮助 命令可用的可用选项:

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. 虚拟机版本

这是为了获取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.系统属性

这将打印为我们的虚拟机设置的所有系统属性。可以显示数百行信息:

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 参数,无论是我们给出的还是 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_lineVM.uptimeVM.dynlibs , 也提供有关所使用的各种其他属性的其他基本且有用的详细信息。

以上所有命令主要是为了获取不同的 JVM 相关详细信息。现在让我们看看更多一些命令,它们可以帮助解决与 JVM 相关的问题。

4.4. 螺纹打印

该命令用于获取即时线程转储。因此,它将打印所有正在运行的线程的堆栈跟踪。以下是它的使用方法,根据使用的线程数量,它可以提供很长的输出:

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([email protected]/Native Method)
    at java.lang.ref.Reference.processPendingReferences([email protected]/Reference.java:241)
    at java.lang.ref.Reference$ReferenceHandler.run([email protected]/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([email protected]/Native Method)
    - waiting on <0x00000000f7330898> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove([email protected]/ReferenceQueue.java:155)
    - waiting to re-lock in wait() <0x00000000f7330898> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove([email protected]/ReferenceQueue.java:176)
    at java.lang.ref.Finalizer$FinalizerThread.run([email protected]/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 ([email protected])
   2:         38656         927744  java.lang.String ([email protected])
   3:          6489         769520  java.lang.Class ([email protected])
   4:         21497         687904  java.util.concurrent.ConcurrentHashMap$Node ([email protected])
   5:          6570         578160  java.lang.reflect.Method ([email protected])
   6:          6384         360688  [Ljava.lang.Object; ([email protected])
   7:          9668         309376  java.util.HashMap$Node ([email protected])
   8:          7101         284040  java.util.LinkedHashMap$Entry ([email protected])
   9:          3033         283008  [Ljava.util.HashMap$Node; ([email protected])
  10:          2919         257000  [I ([email protected])
  11:           212         236096  [Ljava.util.concurrent.ConcurrentHashMap$Node; ([email protected])

但是,如果这不能提供清晰的图片,我们可以获得堆转储。我们接下来看一下。

4.6. G C.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 命令选项

在我们之前的文章中,我们讨论了使用 JFRJMC 进行 Java 应用程序监控。现在,让我们研究一下可用于分析应用程序性能问题的 jcmd 命令。

JFR (或 Java Flight Recorder)是 JDK 中内置的分析和事件收集框架。 JFR 允许我们收集有关 JVM 和 Java 应用程序行为方式的详细低级信息。此外,我们可以使用 JMC 来可视化 JFR 收集的数据。因此, JFRJMC 共同创建了一个完整的工具链来持续收集低级和详细的运行时信息。

尽管如何使用 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 应用程序所在的同一位置创建了一个示例 JFR 记录文件,名称为 demorecording.jfr 。此外,该录音时长为 20 秒,并根据要求进行配置。

此外,我们可以使用 JFR.check 命令检查 JFR 记录的状态。而且,我们可以使用 JFR.stop 命令立即停止并放弃录制。另一方面, JFR.dump 命令可用于立即停止并转储录制。

4.8. VM.native_内存

这是最好的命令之一,可以提供有关 JVM 上 堆和非堆 内存的大量有用详细信息。因此,这可用于调整内存使用情况并检测任何内存泄漏。众所周知,JVM内存大致可以分为堆内存和非堆内存。要获取完整的 JVM 内存使用情况的详细信息,我们可以使用此实用程序。此外,这对于定义基于容器的应用程序的内存大小很有用。

要使用此功能,我们需要使用附加 VM 参数重新启动应用程序,即 – 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 堆内存 之外的不同内存类型的详细信息。 Class 定义了用于存储类元数据的 JVM 内存。同样, 线程 定义了我们的应用程序线程正在使用的内存。并且 Code 给出了用于存储*JIT-*生成的代码的内存, Compiler 本身也有一些空间使用, GC 也占用了一些空间。

此外, reserved 可以估算出我们的应用程序所需的内存。 提交的 显示的是最小分配的内存。

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 的工作,我们会注意到内存使用量的增加和减少。但是,如果内存使用量不受控制地增加,则可能是内存泄漏问题。因此,我们可以从这些统计信息中识别内存泄漏区域,如 线程、代码、类 等。如果我们的应用程序需要更多内存,我们可以分别调整相应的VM参数。

如果内存泄漏在 Heap 中,我们可以进行堆转储(如前所述)或者只是调整 Xmx 。同样,如果内存泄漏发生在 Thread 中,我们 可以查找未处理的递归指令或调整 Xss

六,结论

在本文中,我们介绍了一个针对不同场景诊断 JVM 的实用程序。

我们还介绍了 jcmd 命令及其获取堆转储、线程转储、JFR 记录以进行各种性能相关分析的各种用法。最后,我们还研究了一种使用 jcmd 诊断内存泄漏的方法。