一、简介

我们在进行 JVM 调优的时候,通常需要借助一些工具来对系统目前的情况进行分析,从而确定当前的 JVM 是否需要进行调优,以及对哪方面进行调优。

关于 JVM 调优的工具主要分为两大类:

  1. 命令工具
    • jps:查看进程的状态信息。
    • jinfo:查看进程基本信息,包括启动参数、垃圾回收期类型等。
    • jstack:查看 Java 进程内线程的堆栈信息。
    • jmap:查看堆栈信息。
    • jhat:堆转储快照分析工具。
    • jstat:JVM 统计监测工具。
  2. 可视化工具
    • jconsole:用于对 JVM 的内存,线程,类的监控。
    • VisualVM:能够监控线程、内存情况。

下面我们就对上面这些工具进行详细介绍。


二、命令工具

2.1 jps

jps 命令,主要用于查看当前正在运行的 Java 进程的信息。命令用法如下:

# -l选项非必填,主要用于查看更详细的进程信息
jps -l

为了方便理解,举个例子,代码如下所示:

public static void main(String[] args) {
    new Thread(() -> {
        while (true) {

        }
    }, "t1").start();
    new Thread(() -> {
        while (true) {

        }
    }, "t2").start();
    new Thread(() -> {
        while (true) {

        }
    }, "t3").start();
}

代码中 main() 方法中创建了三个线程:t1、t2、t3。这三个线程中都使用了一个 while (true) 死循环,我们执行一下当前 main() 方法,如下所示:

打开命令行窗口,输入 jps 命令,可以查看当前正在运行的所有的 Java 程序 ,如下所示:

其中 25848 Main 就是我们刚才执行的 main() 方法。以上就是 jps 命令,主要用于查看 Java 进程的信息。

补充: 如果使用 jps 命令看不到完整的 Jar 包名称,可以使用 jps -l 命令。

在这里插入图片描述

2.2 jinfo

jinfo命令,主要用于实时查看和修改正在运行的 Java 应用程序的 JVM 参数和系统属性。

语法如下:

# 查看完整参数
jinfo <pid>
# 查看所有 JVM 标识及其当前值(-禁用+启用)
jinfo -flags <pid>

我们可以使用 java -flags <pid> 来查看当前 Java 进程使用的垃圾回收器类型,如下图所示:

在这里插入图片描述

2.3 jstack

jstack 命令,主要用于查看 Java 进程内线程的堆栈信息。用法如下所示:

# pid 就是上面我们用 jps 命令查看到的 Java 进程ID
jstack [options] <pid>

举个例子,上面我们已经用 jps 看到了 main() 方法所在 Java 进程的 PID 是 25848,所以我们直接使用如下命令:

jstack 25848

执行结果如下:

PS D:\IdeaProjects\SpringBootExamples\springboot-demo> jstack 25848
2024-04-16 13:19:58
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.60-b23 mixed mode):

"DestroyJavaVM" #23 prio=5 os_prio=0 tid=0x00000000034a4800 nid=0x64e4 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"t3" #22 prio=5 os_prio=0 tid=0x0000000022341000 nid=0xebc runnable [0x000000002301f000]
   java.lang.Thread.State: RUNNABLE
        at com.demo.test.Main.lambda$main$6(Main.java:17)
        at com.demo.test.Main$$Lambda$3/2121055098.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)

"t2" #21 prio=5 os_prio=0 tid=0x0000000022340000 nid=0x8be0 runnable [0x0000000022f1f000]
   java.lang.Thread.State: RUNNABLE
        at com.demo.test.Main.lambda$main$5(Main.java:12)
        at com.demo.test.Main$$Lambda$2/933699219.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)

"t1" #20 prio=5 os_prio=0 tid=0x000000002233f800 nid=0x1518 runnable [0x0000000022e1f000]
   java.lang.Thread.State: RUNNABLE
        at com.demo.test.Main.lambda$main$4(Main.java:7)
        at com.demo.test.Main$$Lambda$1/932172204.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)

"Service Thread" #19 daemon prio=9 os_prio=0 tid=0x000000002207f000 nid=0x7338 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread11" #18 daemon prio=9 os_prio=2 tid=0x000000001fa4c800 nid=0x76e8 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread10" #17 daemon prio=9 os_prio=2 tid=0x000000001fa51800 nid=0x8f4c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread9" #16 daemon prio=9 os_prio=2 tid=0x000000001fa4d800 nid=0x2fa8 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread8" #15 daemon prio=9 os_prio=2 tid=0x000000001fa53800 nid=0x6f58 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread7" #14 daemon prio=9 os_prio=2 tid=0x000000001fa53000 nid=0x8718 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread6" #13 daemon prio=9 os_prio=2 tid=0x000000001fa4a000 nid=0xfd0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread5" #12 daemon prio=9 os_prio=2 tid=0x000000001fa35000 nid=0x7930 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread4" #11 daemon prio=9 os_prio=2 tid=0x000000001fa29000 nid=0x1c04 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x000000001fa27800 nid=0x70a8 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000001fa26800 nid=0x7dcc waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000000001fa24000 nid=0x8584 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000000001fa12000 nid=0x32f0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001fa30800 nid=0x3e14 runnable [0x000000002131e000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:170)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        - locked <0x000000076e4f8ed8> (a java.io.InputStreamReader)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at java.io.BufferedReader.fill(BufferedReader.java:161)
        at java.io.BufferedReader.readLine(BufferedReader.java:324)
        - locked <0x000000076e4f8ed8> (a java.io.InputStreamReader)
        at java.io.BufferedReader.readLine(BufferedReader.java:389)
        at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:56)

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001f7cb800 nid=0x6ff8 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001f81c800 nid=0x93b4 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001dd33000 nid=0x6848 in Object.wait() [0x000000002101f000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076e1070b8> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
        - locked <0x000000076e1070b8> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000000001dd2a800 nid=0x6f90 in Object.wait() [0x000000000291f000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076e106af8> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157)
        - locked <0x000000076e106af8> (a java.lang.ref.Reference$Lock)

"VM Thread" os_prio=2 tid=0x000000001f784800 nid=0x652c runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00000000034b9800 nid=0x2c08 runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00000000034bb800 nid=0x8e30 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00000000034bd000 nid=0x7e0c runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00000000034be800 nid=0x6774 runnable

"GC task thread#10 (ParallelGC)" os_prio=0 tid=0x00000000034ca800 nid=0x7fe8 runnable

"GC task thread#11 (ParallelGC)" os_prio=0 tid=0x00000000034ce000 nid=0x68c runnable

"GC task thread#12 (ParallelGC)" os_prio=0 tid=0x00000000034cf000 nid=0x949c runnable

"VM Periodic Task Thread" os_prio=2 tid=0x000000001fa12800 nid=0x555c waiting on condition

JNI global references: 320

这个信息比较多,它会把当前 Java 进程所对应的所有线程信息打印出来,其中 t1、t2、t3 就是我们刚才自己创建的线程,如下所示:

在这里插入图片描述

信息中展示了当前线程对应的是哪一行代码在运行。

在这里插入图片描述

假如说程序产生了死锁之后,我们就可以使用当前的 jstack 命令查看当前线程运行的情况,当然也可以查看其它状态线程的运行情况。

以上就是 jstack 命令的使用情况,主要用于查看 Java 进程对应的线程运行情况。

2.4 jmap

jmap 命令,在实际开发用得比较多,主要用于生成堆转内存快照,方便查看内存的使用情况。用法如下:

# 显示 Java 堆的信息
jmap -heap <pid>
# 将 Java 堆信息导出为文件
jmap -dump:format=b,file=heap.hprof <pid>
  • format=b:表示以 hprof 二进制格式转储 Java 堆的内存。
  • file=<filename> 用于指定快照 dump 文件的文件名。
用法1:查看堆内存使用情况

举个例子,我们继续使用上面的进程ID 25848,执行如下命令:

jmap -heap 25848

执行结果如下所示:

打印的堆信息是比较多的,并且不太好阅读,为了方便理解,这里手动添加了注释。

PS D:\IdeaProjects\SpringBootExamples\springboot-demo> jmap -heap 25848
Attaching to process ID 25848, please wait...
Debugger attached successfully.
Server compiler detected.
Heap Configuration: // 堆配置
   MinHeapFreeRatio         = 0     // 空闲堆空间的最小百分比
   MaxHeapFreeRatio         = 100   // 空闲堆空间的最大百分比
   MaxHeapSize              = 4125097984 (3934.0MB) // 堆空间允许的最大值
   NewSize                  = 85983232 (82.0MB)     // 新生代堆空间的默认值
   MaxNewSize               = 1374683136 (1311.0MB) // 新生代堆空间允许的最大值
   OldSize                  = 171966464 (164.0MB)   // 老年代堆空间的默认值
   NewRatio                 = 2 // 新生代与老年代的堆空间比值,新生代:老年代=1:2
   SurvivorRatio            = 8 // 两个Survivor区和Eden区的堆空间比值为8,表示S0:S1:Eden=1:1:8
   MetaspaceSize            = 21807104 (20.796875MB) // 元空间的默认值
   CompressedClassSpaceSize = 1073741824 (1024.0MB)  // 压缩卷使用空间大小
   MaxMetaspaceSize         = 17592186044415 MB // 元空间允许的最大值
   G1HeapRegionSize         = 0 (0.0MB) // 在使用G1垃圾回收器时,JVM会将Heap空间分隔为若干个Region,该参数用来指定每个Region空间的大小。

Heap Usage: // 堆使用情况
PS Young Generation
Eden Space: // Eden区使用情况
   capacity = 65011712 (62.0MB)
   used     = 15607624 (14.884590148925781MB)
   free     = 49404088 (47.11540985107422MB)
   24.007403466009325% used
From Space: // Survivor-From区使用情况
   capacity = 10485760 (10.0MB)
   used     = 0 (0.0MB)
   free     = 10485760 (10.0MB)
   0.0% used
To Space: // Survivor-To区使用情况
   capacity = 10485760 (10.0MB)
   used     = 0 (0.0MB)
   free     = 10485760 (10.0MB)
   0.0% used
PS Old Generation // 老年代使用情况
   capacity = 171966464 (164.0MB)
   used     = 0 (0.0MB)
   free     = 171966464 (164.0MB)
   0.0% used

3512 interned Strings occupying 286240 bytes.

上面我们介绍完了使用 jmap 命令直接查看当前堆内存情况的用法,下面我们再来介绍一下 jmap 的导出用法。

用法2:导出 dump 文件

导出命令语法如下:

# 将 Java 堆信息导出为文件
jmap -dump:format=b,file=heap.hprof <pid>
  • format=b:表示以 hprof 二进制格式转储 Java 堆的内存。
  • file=<filename> 用于指定快照 dump 文件的文件名。

概括来说,就是使用 jmap 命令将堆内存生成一个快照,这个快照是一个 dump 二进制文件。

详细来说,生成的 dump 文件是一个进程或系统在某一给定时间的快照。比如在进程崩溃时,甚至是任何时候,我们都可以通过工具将系统或某进程的内存备份出来,供调试分析用。dump 文件中包含了:程序运行的模块信息、线程信息、堆栈调用信息、异常信息等数据,方便系统技术人员进行错误排查。

举个例子,我们继续使用上面的示例来进行操作,执行如下命令:

jmap -dump:format=b,file=d:/hprof/myDump.hprof 25848

执行结果如下所示:

在这里插入图片描述

我们来到 d:/hprof/ 目录下,可以看到 dump 文件已经正常生成了,如下所示:

在这里插入图片描述

这个 dump 文件可以使用 Eclipse 的 MAT 工具进行分析查看,内容较多,这里就不再介绍了。

以上就是 jmap 命令的使用介绍,它主要用于查看堆内存的使用情况,导出堆内存的 dump 文件。

2.5 jstat

jstat 命令,是 JVM 统计检测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。

用法1:统计gc信息百分比

语法如下:

jstat -gcutil <pid>

执行结果:

在这里插入图片描述

  • S0:年轻代中第一个survivor(幸存区)已使用的占当前容量百分比。
  • S1:年轻代中第二个survivor(幸存区)已使用的占当前容量百分比。
  • E:年轻代中Eden(伊甸园)已使用的占当前容量百分比。
  • O:old代已使用的占当前容量百分比。
  • M:元数据区已使用的占当前容量百分比。
  • CCS:压缩类空间已使用的占当前容量百分比。
  • YGC :从应用程序启动到采样时年轻代中gc次数。
  • YGCT :从应用程序启动到采样时年轻代中gc所用时间(s)。
  • FGC :从应用程序启动到采样时old代(全gc)gc次数。
  • FGCT :从应用程序启动到采样时old代(全gc)gc所用时间(s)。
  • GCT:从应用程序启动到采样时gc用的总时间(s)。
用法2:统计gc的次数及时间

语法如下:

jstat -gc <pid>

执行结果:

在这里插入图片描述

  • S0C:年轻代中第一个survivor(幸存区)的容量 (字节)。
  • S1C:年轻代中第二个survivor(幸存区)的容量 (字节)。
  • S0U:年轻代中第一个survivor(幸存区)目前已使用空间 (字节)。
  • S1U:年轻代中第二个survivor(幸存区)目前已使用空间 (字节)。
  • EC:年轻代中Eden(伊甸园)的容量 (字节)。
  • EU:年轻代中Eden(伊甸园)目前已使用空间 (字节)。
  • OC:老年代的容量 (字节)。
  • OU:老年代目前已使用空间 (字节)。
  • MC:metaspace(元空间)的容量 (字节)。
  • MU:metaspace(元空间)目前已使用空间 (字节)。
  • CCSC:当前压缩类空间的容量 (字节)。
  • CCSU:当前压缩类空间目前已使用空间 (字节)。
  • YGC:从应用程序启动到采样时年轻代中gc次数。
  • YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)。
  • FGC:从应用程序启动到采样时老年代(全gc)gc次数。
  • FGCT:从应用程序启动到采样时老年代(全gc)gc所用时间(s)。
  • GCT:从应用程序启动到采样时gc用的总时间(s)。

以上就是常用的命令工具讲解了,下面我们介绍几个常用的可视化工具。


三、可视化工具

3.1 jconsole

jconsole 工具,主要用于对 JVM 的内存、线程、类进行监控,是一个基于 JMX 的 GUI 性能监控工具。

打开方式:java 安装目录 bin 目录下,如下所示:

在这里插入图片描述

直接双击启动 jconsole.exe 就可以,找到我们想要观察的 Java 进行,点击 “连接”。

点击选择 “不安全的连接”:

现在我们就能看到详细的 Java 进程信息了:

在这里插入图片描述

当然,除了概览,我们还可以查看内存、线程、类等资源的使用情况。

在这里插入图片描述


3.2 VisualVM

VisualVM工具,能够监控线程、内存情况,查看方法的 CPU 时间和内存中的对象、已被GC的对象,也可以反向查看分配的堆栈。

打开方式:java 安装目录 bin 目录下,直接双击启动 jvisualvm.exe 就行,这个工具目前只有 JDK8 版本自带,其它版本需要使用可以到 Java 官方进行下载。

在这里插入图片描述

双击打开之后,界面如下所示:

在这里插入图片描述

双击想要查看的 Java 进程即可进行查看:

在这里插入图片描述

VisualVM 提供了非常详细的数据展示,供我们对进程中线程、堆、栈的问题进行排查。

在这里插入图片描述

以上就是常用 JVM 调优用的全部工具介绍。

整理完毕,完结撒花~ 🌻

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐