深入浅出 JVM 之调优 - JVM 命令行工具

深入浅出 JVM 之调优 - JVM 命令行工具

文章目录

  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。

系统环境:

  • JDK 1.8

深入浅出 JVM 系列文章

一、常用 JVM 命令概述

在安装完 JDK 环境后,其自身就带一些常用的调试 JVM 调试工具,如下:

  • jps: 用于列出当前系统中所有的 Java 进程,以及它们的进程 ID。
  • jstat: 用于监控 JVM 的各种统计数据,例如垃圾回收、类装载、线程、内存等。
  • jinfo: 用于查看和调整 JVM 进程的运行时配置信息,例如堆内存大小、垃圾回收器等。
  • jstack: 用于生成当前 JVM 进程的线程堆栈信息,以便于定位死锁或性能问题。
  • jmap: 用于生成 JVM 内存映像文件,以便于分析内存使用情况和调试内存泄漏等问题。

这些工具都是 JDK 自带的命令行工具,只需要简单几条命令,就可以很方便的帮助我们开发者定位 JVM 一些问题。比如查看新生代/老年代空间占用率、查看加载的类、查看堆栈信息、分析死锁及内存泄漏原因等等。

可能很多人都会说现在又很多可视化的 JVM GUI 工具,为啥还要用 JVM 命令行工具呢?其实现在线上的项目很多时候,部署后有诸多限制,很难使用 JVM GUI 工具去分析,但是大部分情况下都有 JDK 环境,可以使用里面的 JVM 命令行工具,使用这些工具可以为我们解决线上问题提供很大的帮助。

二、常用 JVM 命令介绍

2.1 jps 命令

jps 简介

jps 命令可以列出当前系统中所有正在运行的 Java 进程的进程 ID (即 JVM 的 PID) 和进程名 (即主类名或进程启动时指定的进程名称)。它可以用于检查某个 Java 应用程序是否在运行,以及查找某个进程的 PID。

jps 选项

1$ jps [-q] [-mlvV] [<hostid>]

命令中的 [-q] 和 [-mlvV] 都是选项,而 [<hostid>] 则表示主机地址,默认不输入则表示输出本地主机信息。

  • -q: 以紧凑的格式显示,只输出进程ID,不输出进程名。
  • -m: 输出进程名和启动时传递给主类 main() 方法的参数。
  • -l: 输出进程名和完整的启动命令,包括 Java 命令和所有参数。
  • -v: 输出进程名、启动命令和JVM启动参数,如-Xms、-Xmx、-XX等。

注: 命令默认是在本地主机执行的,但是 jps 命令也支持远程主机,所以这里的 <hostid> 表示远程主机,远程主机上使用 jps 命令需要确保在远程主机上启动了 RMI(远程方法调用) 服务,并且在本地主机上配置了与远程主机的信任关系。

jps 示例

例如,以下是一些常用的jps命令示例:

 1## 列出当前系统中所有正在运行的Java进程的PID和进程名。
 2$ jps
 3
 4## 列出当前系统中所有正在运行的Java进程的PID。
 5$ jps -q
 6
 7## 列出当前系统中所有正在运行的Java进程的PID、进程名和启动时传递给主类main()方法的参数。
 8$ jps -m
 9
10## 列出当前系统中所有正在运行的Java进程的PID、进程名和完整的启动命令。
11$ jps -l
12
13## 列出当前系统中所有正在运行的Java进程的PID、进程名、启动命令和JVM启动参数。
14$ jps -v
15
16## 以紧凑的格式显示进程ID和类名。
17$ jps -q 
18
19## 显示当前运行的所有Java进程的完整类名、传递给Java虚拟机的参数以及Java应用程序的参数。
20$ jps -lvm 

需要注意的是,jps 命令只能列出当前系统中正在运行的 Java 进程,而不能列出已经停止或崩溃的进程。如果需要查看更详细的 Java 进程信息,可以使用 jcmd 命令或 VisualVM 工具。

2.2 jstat 命令

jstat 简介

jstat 命令可以查看 Java 应用程序中与 JVM 相关的各项运行数据。比如查看内存、垃圾回收、类加载等信息,可以通过不同的选项来获取不同的数据。同时,也可以通过指定不同的时间间隔和次数来查看历史数据和趋势分析。

jstat 选项

1$ jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

该命令支持的参数如下:

  • <option>: 必选参数,表示要查询的选项。
  • -t: 可选参数,表示输出结果时显示时间戳,单位为秒。
  • -h<lines>: 可选参数,表示输出结果时显示的行数,不指定则显示全部行。
  • <vmid>: 必选参数,表示要查询的 JVM 进程的进程ID或进程名。
  • <interval>: 可选参数,表示查询的时间间隔,单位为毫秒,默认值为1000毫秒。
  • <count>: 可选参数,表示查询的次数,默认值为无限次。

其中 <option> 选项支持配置的参数如下:

  • -gc: 显示垃圾回收相关的信息,包括堆内存的使用情况和 GC 时间等。
  • -class: 显示类加载、卸载、卸载失败的数量以及卸载的时间。
  • -compiler: 显示 JIT 编译器的编译情况。
  • -gccapacity: 显示 JVM 各个区域 (新生代、老年代等) 的容量。
  • -gcnew: 显示新生代统计信息。
  • -gcold: 显示老年代和元空间统计信息。
  • -gcnewcapacity: 显示新生代大小统计信息。
  • -gcoldcapacity: 显示老年代及元空间大小的统计信息。
  • -gcmetacapacity: 显示有关元空间大小的统计信息。
  • -gcutil: 显示垃圾回收相关的信息,包括 GC 时间、吞吐量、最大停顿时间等。
  • -printcompilation: 显示 JIT 编译器的详细编译日志,包括已编译方法的名称、状态、编译时间等信息。

jstat 不同选项输出的参数

执行命令时配置不同参数将返回不同的结果,由于返回的结果中大部分都是简写,让人不明其意,这里将对配置不同选项得到的结果进行说明:

-gc:

  • S0C: 新生代 Survivor-0 区的空间大小(字节)。
  • S1C: 新生代 Survivor-1 区的空间大小(字节)。
  • S0U: 新生代 Survivor-0 区已使用的空间大小(字节)。
  • S1U: 新生代 Survivor-1 区已使用的空间大小(字节)。
  • TT: Tenuring Threshold,即表示晋升老年代阈值。
  • MTT: Maximum Tenuring Threshold,即表示晋升老年代的最大阈值。
  • DSS: 期望新生代空间大小。
  • EC: 新生代 Eden 区空间大小(字节)。
  • EU: 新生代 Eden 区已使用的空间大小(字节)。
  • YGC: Young GC 已经执行的次数。
  • YGCT: Young GC 所花费的总时间(秒)。

-class:

  • Loaded: 已加载类的数量,包括 Java 类、方法区常量以及静态变量等。
  • Bytes: 已加载类占用的字节数大小,包括 Java 类、方法区常量以及静态变量等。
  • Unloaded: 已卸载类的数量。
  • Bytes: 已卸载类占用的字节数大小。
  • Time: 执行类装入和卸载操作所花费的时间。

-compiler:

  • Compiled: 已经编译的方法数量。
  • Failed: 编译失败的方法数量。
  • Invalid: 编译的无效方法数量,包括被丢弃的方法、没有编译的方法和编译后被丢弃的方法等。
  • Time: 编译所花费的时间。
  • FailedType: 编译失败类型,包括无法解析、不支持、异常、无法内联、无法优化等。
  • FailedMethod: 编译失败所在类及方法名。

-gccapacity:

  • NGCMN: 新生代空间的最小容量(字节)。
  • NGCMX: 新生代空间的最大容量(字节)。
  • NGC: 新生代当前容量大小(字节)。
  • S0C: 新生代 Survivor-0 区的空间大小(字节)。
  • S1C: 新生代 Survivor-1 区的空间大小(字节)。
  • EC: 新生代 Eden 区空间容量(字节)。
  • OGCMN: 老年代空间的最小容量(字节)。
  • OGCMX: 老年代空间的最大容量(字节)。
  • OGC: 老年代当前的空间容量大小(字节)。
  • OC: 老年代的空间容量大小(字节)。
  • MCMN: 元空间最小大小(字节)。
  • MCMX: 元空间最大大小(字节)。
  • MC: 元空间当前大小(字节)。
  • CCSMN: 压缩类空间最小大小(字节)。
  • CCSMX: 压缩类空间最大大小(字节)。
  • CCSC: 压缩类空间当前大小(字节)。
  • YGC: Young GC 的次数。
  • FGC: Full GC 的次数。
  • CGC: 并发 Concurrent GC 的次数。

-gcnew:

  • S0C: 新生代 Survivor-0 区的空间大小(字节)。
  • S1C: 新生代 Survivor-1 区的空间大小(字节)。
  • S0U: 新生代 Survivor-0 区已使用的空间大小(字节)。
  • S1U: 新生代 Survivor-1 区已使用的空间大小(字节)。
  • TT: Tenuring threshold,即表示晋升老年代阈值。
  • MTT: Maximum tenuring threshold,即表示晋升老年代的最大阈值。
  • DSS: 在整个垃圾回收期间保持不变的代的数量。
  • EC: 新生代 Eden 区空间大小(字节)。
  • EU: 新生代 Eden 区已使用的空间大小(字节)。
  • YGC: Young GC 已经执行的次数。
  • YGCT: Young GC 所花费的总时间(秒)。

-gcold:

  • MC: 元空间当前大小(字节)。
  • MU: 元空间的使用量(字节)。
  • CCSC: 压缩类空间的当前容量。
  • CCSU: 压缩类空间的使用量。
  • OC: 老年代的空间容量大小(字节)。
  • OU: 老年代已使用空间大小(字节)。
  • YGC: Young GC 已经执行的次数。
  • FGC: Full GC 已经执行的次数。
  • FGCT: Full GC 所花费的总时间(秒)。
  • CGC: 并发 Concurrent GC 已经执行的次数。
  • CGCT: 并发 Concurrent GC 所花费的总时间(秒)。
  • GCT: 所有 GC 所花费的总时间(秒)。

-gcnewcapacity:

  • NGCMN: 新生代空间的最小容量(字节)。
  • NGCMX: 新生代空间的最大容量(字节)。
  • NGC: 新生代当前容量大小(字节)。
  • S0CMX: 新生代 Survivor-0 区的空间最大容量(字节)。
  • S0C: 新生代 Survivor-0 区的空间大小(字节)。
  • S1CMX: 新生代 Survivor-1 区的空间最大容量(字节)。
  • S1C: 新生代 Survivor-1 区的空间大小(字节)。
  • ECMX: 新生代 Eden 区的最大容量大小(字节)。
  • EC: 新生代 Eden 区空间大小(字节)。
  • YGC: Young GC 已经执行的次数。
  • FGC: Full GC 已经执行的次数。
  • CGC: 并发 Concurrent GC 已经执行的次数。

-gcoldcapacity:

  • OGCMN: 老年代空间的最小容量(字节)。
  • OGCMX: 老年代空间的最大容量(字节)。
  • OGC: 老年代当前的空间容量大小(字节)。
  • OC: 老年代的空间容量大小(字节)。
  • YGC: Young GC 已经执行的次数。
  • FGC: Full GC 已经执行的次数。
  • FGCT: Full GC 所花费的总时间(秒)。
  • CGC: 并发 Concurrent GC 已经执行的次数。
  • CGCT: 并发 Concurrent GC 所花费的总时间(秒)。
  • GCT: 所有 GC 所花费的总时间(秒)。

-gcmetacapacity:

  • MCMN: 元空间最小大小(字节)。
  • MCMX: 元空间最大大小(字节)。
  • MC: 元空间当前大小(字节)。
  • CCSMN: 压缩类空间最小大小(字节)。
  • CCSMX: 压缩类空间最大大小(字节)。
  • CCSC: 压缩类空间当前大小(字节)。
  • YGC: Young GC 已经执行的次数。
  • FGC: Full GC 已经执行的次数。
  • FGCT: Full GC 所花费的总时间(秒)。
  • CGC: 并发 Concurrent GC 已经执行的次数。
  • CGCT: 并发 Concurrent GC 所花费的总时间(秒)。
  • GCT: 所有 GC 所花费的总时间(秒)。

-gcutil:

  • S0: 新生代 survivor-0 区已使用的容量比例。
  • S1: 新生代 survivor-1 区已使用的容量比例。
  • E: 新生代 Eden 区已使用的容量比例。
  • O: 老年代 Old 区已使用的容量比例。
  • M: 元空间 Metaspace 已使用的容量比例。
  • CCS: 压缩类空间已使用的容量比例。
  • YGC: Young GC 已经执行的次数。
  • YGCT: Young GC 执行所花费的总时间(秒)。
  • FGC: Full GC 已经执行的次数。
  • FGCT: Full GC 所花费的总时间(秒)。
  • CGC: 并发 Concurrent GC 已经执行的次数。
  • CGCT: 并发 Concurrent GC 所花费的总时间(秒)。
  • GCT: 全部 GC 所花费的总时间(秒)。

-printcompilation:

  • Compiled: 执行编译的次数。
  • Size: 编译后的代码大小。
  • Type: 编译类型,分别有以下四种类型:
    • C1: Client Compiler;
    • C2: Server Compiler;
    • C2N: Server Compiler (no tiered compilation);
    • C1/C2: Client and Server Compiler;
  • Method: 已编译方法的名称,这是一种带有类名和方法名的内部格式。可以使用 jmap -print 命令将其转换为常规格式。

jstat 示例

 1## 每隔 5 秒输出 JVM 垃圾回收情况
 2$ jstat -gcutil <vmid> 5000
 3
 4## 每隔 5 秒输出 JIT 编译器的运行情况
 5$ jstat -printcompilation <vmid> 5000
 6
 7## 每隔 5 秒输出类加载情况
 8$ jstat -class <vmid> 5000
 9
10## 每隔 5 秒输出线程情况
11$ jstat -t <vmid> <5000
12
13## 每隔 5 秒输出垃圾回收器堆内存使用情况
14$ jstat -gc <vmid> <interval> <count>
15
16## 监视新生代内存接下来 30 分钟的使用情况,每隔 5 秒输出一次
17$ jstat -gcnew <vmid> 5000 6

2.3 jinfo 命令

jinfo 简介

jinfo 命令用于展示 JVM 的配置和系统属性。它可以用来调试 JVM 的问题,也可以用来监控 JVM 在运行时的配置。

jinfo 选项

1$ jinfo <option> <pid> 

这里 <pid> 指的是 JVM 进程 ID,而 <option> 则是支持的可配选项,如下:

  • -flag: 查看指定 JVM 参数的值。
  • -flag [+|-]<name>: 启用(+)或禁用(-)指定的 JVM 参数。
  • -flag <name>=: 将命指定名称的 JVM 参数设置为给定的值。
  • -flags: 输出 JVM 参数。
  • -sysprops: 输出 JVM 系统属性。

如果没有配置任何选项,则默认打印 VM 标志和系统属性。

jinfo 示例

 1## 查看JVM中的系统属性:
 2$ jinfo -sysprops <pid>
 3
 4## 查看指定flag的值,比如查询参数 PrintGCDetails 值
 5$ jinfo -flag PrintGCDetails <pid>
 6
 7## 修改指定flag的值,比如修改参数 PrintGCDetails 值
 8$ jinfo -flag +PrintGCDetails <pid>
 9
10## 查看JVM的版本信息
11$ jinfo -version <pid>

2.4 jstack 命令

jstack 简介

jstack 命令用于查看 Java 进程中线程的堆栈信息,使用该命令可以协助开发人员排查应用程序中出现的死锁、卡死等问题。

jstack 选项

1$ jstack [-l][-e] <pid>

这里 <pid> 指的是 JVM 进程 ID,而 [-l] 和 [-e] 则是支持的可配选项,如下:

  • -l: 输出锁的附加信息。
  • -e: 输出线程的附加信息。

jstack 示例

1## 打印所有线程的详细堆栈信息,包括持有锁和等待锁的线程
2$ jstack -l <pid>
3
4## 打印出现异常的线程的堆栈信息
5$ jstack -e <pid>
6
7## 打印出现异常的线程的堆栈信息,并输出到 jstack.log 文件中
8$ jstack -e <pid> > jstack.log

2.5 jmap 命令

jmap 简介

jmap 命令用于在运行时生成 JVM 内存快照,并显示 Java 进程内存使用的详细信息。可以通过 jmap 工具来了解 Java 进程的内存使用情况,排查内存泄漏等问题。

jmap 选项

1$ jmap <option> <pid>

这里的 <option> 支持的选项如下:

  • -heap: 输出堆内存 (Heap) 的详细信息,包括大小、最大值、空闲空间等。
  • -clstats: 输出类加载器统计信息。
  • -histo[:live]: 输出 Java 进程中对象的数量及其占用内存的信息,其中后面追加 :live 参数可以过滤死亡对象,只保留存活对象的统计。
  • -finalizerinfo: 输出等待终结器(Finalizer)线程的对象和数量。
  • -dump:file=<heap_dump_file> 生成 Java 进程的 HeapDump 文件,它保存了 Java 进程在某个时刻内存中的所有内容,包括 Java 对象、类信息、线程、堆栈等。可以使用堆转储文件进行内存泄漏分析、内存调优、资源分析等。

jmap 示例

 1## 将指定进程中的堆信息存入dump.hprof文件,并保存在/tmp/目录下
 2$ jmap -dump:file=/tmp/dump.hprof <pid>
 3
 4## 查看指定进程中类加载器统计信息
 5$ jmap -clstats <pid>
 6
 7## 查看指定进程中堆内存使用情况
 8$ jmap -heap <pid>
 9
10## 查看堆内存使用情况,包括大小、最大值、空闲空间等
11$ jmap -finalizerinfo <pid>
12
13## 查看指定进程中,存活对象的数量和内存占用等
14$ jmap -histo:live <pid>
15
16## 查看指定进程中finalizer线程信息,输出finalizer线程的对象和数量
17$ jmap -finalizerinfo <pid>

三、常用的 JVM 命令解决问题示例

3.1 示例1: 使用 jmap 查看内存情况

查找 Java 应用进程 ID

使用 jps 命令查找 Java 进程 ID,命令如下:

1$ jps -l
2
320040 app.jar
47149 Jps

使用 jmap 命令查看堆内存使用情况

使用 jmap 命令可以很方便的查看当前 Java 应用堆内存使用情况,命令如下:

 1$ jmap -heap 20040
 2
 3Attaching to process ID 20040, please wait...
 4Debugger attached successfully.
 5Server compiler detected.
 6JVM version is 25.191-b12
 7
 8using thread-local object allocation.
 9Parallel GC with 2 thread(s)
10
11Heap Configuration:
12   MinHeapFreeRatio         = 0
13   MaxHeapFreeRatio         = 100
14   MaxHeapSize              = 478150656 (456.0MB)
15   NewSize                  = 10485760 (10.0MB)
16   MaxNewSize               = 159383552 (152.0MB)
17   OldSize                  = 20971520 (20.0MB)
18   NewRatio                 = 2
19   SurvivorRatio            = 8
20   MetaspaceSize            = 21807104 (20.796875MB)
21   CompressedClassSpaceSize = 1073741824 (1024.0MB)
22   MaxMetaspaceSize         = 17592186044415 MB
23   G1HeapRegionSize         = 0 (0.0MB)
24
25Heap Usage:
26PS Young Generation
27Eden Space:
28   capacity = 83886080 (80.0MB)
29   used     = 76404128 (72.86465454101562MB)
30   free     = 7481952 (7.135345458984375MB)
31   91.08081817626953% used
32From Space:
33   capacity = 3145728 (3.0MB)
34   used     = 2981936 (2.8437957763671875MB)
35   free     = 163792 (0.1562042236328125MB)
36   94.79319254557292% used
37To Space:
38   capacity = 3145728 (3.0MB)
39   used     = 0 (0.0MB)
40   free     = 3145728 (3.0MB)
41   0.0% used
42PS Old Generation
43   capacity = 20447232 (19.5MB)
44   used     = 5970120 (5.693550109863281MB)
45   free     = 14477112 (13.806449890136719MB)
46   29.19769287109375% used
47
4813365 interned Strings occupying 1167016 bytes.

根据输出结果可以观察到新生代/元空间/老年代/压缩空间的内存占用情况,以及已经使用的空间大小和空间的占用率等信息。

3.2 示例2: 使用 jstat 排查内存泄漏

创建 Java 测试用例模拟内存泄漏

首先创建示例项目,模拟发生内存泄漏情况,示例如下:

 1import java.security.SecureRandom;
 2import java.util.ArrayList;
 3import java.util.List;
 4import java.util.Random;
 5import java.util.concurrent.TimeUnit;
 6
 7public class MemoryLeakDemo {
 8
 9    static List<Long> list = new ArrayList<>();
10
11    public static void main(String[] args) throws InterruptedException {
12        while (true) {
13            // 生成 20 以内的随机数 randomNum,然后使 randomNum * 10000
14            SecureRandom random = new SecureRandom();
15            int randomNum = random.nextInt(10);
16            int num = new Random().nextInt(20) * 10000;
17            // 生成 randomNum * 10000 个数值,插入 list 集合
18            for (long i = 0; i < num; i++) {
19                list.add(i);
20            }
21            // 停顿 1 秒
22            TimeUnit.SECONDS.sleep(1);
23        }
24    }
25    
26}

查找 Java 应用进程 ID

使用 jps 命令查找 Java 进程 ID,命令如下:

1$ jps -l
2
317698 app.jar
47149 Jps

每间隔 5 秒统计 GC 变化情况

使用 jstat 命令,每间隔 5 秒就统计一下当前的 GC 的情况,命令如下:

 1$ jstat -gc -t 17698 5000
 2
 3Timestamp S0C  S1C     S0U  S1U     EC       EU     OC       OU       CCSC   CCSU    YGC YGCT  FGC FGCT   CGC CGCT  GCT   
 4853.8     0.0  2048.0  0.0  2048.0  11264.0  7168.0 232448.0 231908.0 4352.0 3727.2  15  0.572 0   0.000  10  0.021 0.593
 5858.8     0.0  2048.0  0.0  2048.0  11264.0  9216.0 232448.0 231908.0 4352.0 3727.2  15  0.572 0   0.000  10  0.021 0.593
 6863.8     0.0  2048.0  0.0  2048.0  12288.0  4096.0 253952.0 226222.5 4352.0 3727.2  17  0.596 0   0.000  12  0.025 0.621
 7868.8     0.0  2048.0  0.0  2048.0  12288.0  0.0    253952.0 248517.0 4352.0 3727.2  19  0.612 0   0.000  12  0.025 0.638
 8873.8     0.0  2048.0  0.0  2048.0  12288.0  9216.0 263168.0 259290.5 4352.0 3727.2  20  0.625 0   0.000  14  0.038 0.663
 9878.8     0.0  1024.0  0.0  1024.0  15360.0  0.0    290816.0 261856.9 4352.0 3733.4  36  0.686 15  2.639  14  0.038 3.363
10883.8     0.0  1024.0  0.0  1024.0  15360.0  0.0    290816.0 263473.7 4352.0 3736.6  59  0.833 39  7.401  14  0.038 8.272
11888.8     0.0  1024.0  0.0  1024.0  15360.0  0.0    290816.0 264199.0 4352.0 3742.3  84  0.885 65  12.234 14  0.038 13.158
12893.8     0.0  1024.0  0.0  1024.0  15360.0  0.0    290816.0 264341.9 4352.0 3742.3  103 1.033 89  17.147 14  0.038 18.218
13898.8     0.0  1024.0  0.0  1024.0  15360.0  0.0    290816.0 264761.9 4352.0 3742.3  124 1.061 115 22.090 14  0.038 23.189
14903.8     0.0  0.0     0.0  0.0     16384.0  0.0    290816.0 265476.5 4352.0 3742.3  127 1.068 120 23.255 14  0.038 24.361

将数据贴到 Excel 中进行统计与分析

(1) 将数据复制到 Excel 中

将上面输出的数据复制到 Excel 中,然后按空格对数据进行分列,完成后就如下图所示:

(2) 对数据选择部分列汇总成图表

① 分析 Young GC: 使 Timestamp + YGCTimestamp + YGCT 列的值绘制图表,统计在指定时间范围内,Young GC 执行次数和花费时长变化趋势:

② 分析 Mixed GC: 使 Timestamp + CGCTimestamp + CGCT 列的值绘制图表,统计在指定时间范围内,Mixed GC 执行次数和花费时长变化趋势:

③ 分析 Mixed GC: 使 Timestamp + FGCTimestamp + FGCT 列的值绘制图表,统计在指定时间范围内,Full GC 执行次数和花费时长变化趋势:

④ 分析总 GC 时长: 使 Timestamp + GCT 列的值绘制图表,统计在指定时间范围内,所有 GC 所花费的时长变化趋势:

⑤ 分析老年代已使用空间占比: 使 Timestamp + OC + OU 列的值绘制图表,统计在指定时间范围内,老年代已使用空间占老年代总空间的变化趋势:

(3) 得出结论

根据上面图表中的变化趋势进行分析,可以了解到 Mixed GC 次数逐渐上升,而 Young GC 和 Full GC 执行次数暴增,Full GC 时长快速上涨,整体 GC 时长也是呈现快速上涨趋势,并且属于老年代的空间划分逐步增多,老年代已使用空间占用老年代总空间的比例逐步上升,每次 GC 后能释放的空间越来越少。根据上面这些情况,可以大胆预测一下,业务代码中可能存在内存泄漏,持续下去就有可能引发内存溢出,导致 OOM 错误发生。

3.3 示例3 使用 jmap 导出堆栈信息

查找 Java 应用进程 ID

使用 jps 命令查找 Java 进程 ID,命令如下:

1$ jps -l
2
362640 app.jar
47149 Jps

导出 Java 进程堆栈信息

1$ jmap -dump:format=b,file=/data/java/app.hprof 62640
2
3Dumping heap to /data/java/app.hprof ...
4Heap dump file created

导出的 Java 进程堆栈信息可以在分析工具中打开,用于分析对象的数量、大小、引用关系等信息,从而找出对象的内存泄漏或过度分配内存的情况。

3.4 示例4: 使用 jstack 排查死锁

创建 Java 测试用例模拟发生死锁

首先创建示例项目,模拟发生死锁情况,示例如下:

 1public class DeadLockDemo {
 2
 3    public static void main(String[] args) {
 4        Object lock1 = new Object();
 5        Object lock2 = new Object();
 6
 7        newThread("ThreadA", lock1, lock2).start();
 8        newThread("ThreadB", lock2, lock1).start();
 9    }
10
11    private static Thread newThread(String threadName, Object lockFirst, Object lockSecond) {
12        return new Thread(() -> {
13            synchronized (lockFirst) {
14                System.out.println(Thread.currentThread().getName() + " holding lockFirst");
15                try {
16                    Thread.sleep(3000L);
17                } catch (InterruptedException e) {
18                    e.printStackTrace();
19                }
20                System.out.println(Thread.currentThread().getName() + " waiting lockSecond");
21                synchronized (lockSecond) {
22                    System.out.println(Thread.currentThread().getName() + " holding lockSecond");
23                }
24
25            }
26        }, threadName);
27    }
28
29}

查找 Java 应用进程 ID

使用 jps 命令查找 Java 进程 ID,命令如下:

1$ jps -l
2
320040 app.jar
47149 Jps

使用 jstack 命令查看线程堆栈信息

使用 jstack 命令查看指定进程中的线程堆栈信息,分析死锁发生原因:

 1$ jstack 20040
 2
 32023-05-07 14:40:39
 4Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.191-b12 mixed mode):
 5
 6"Attach Listener" #30 daemon prio=9 os_prio=0 tid=0x00007f9614001800 nid=0x11f9b waiting on condition [0x0000000000000000]
 7   java.lang.Thread.State: RUNNABLE
 8
 9"DestroyJavaVM" #29 prio=5 os_prio=0 tid=0x00007f9650009800 nid=0x4e49 waiting on condition [0x0000000000000000]
10   java.lang.Thread.State: RUNNABLE
11
12"ThreadB" #28 prio=5 os_prio=0 tid=0x00007f96510ed000 nid=0x4ea1 waiting for monitor entry [0x00007f96379be000]
13   java.lang.Thread.State: BLOCKED (on object monitor)
14        at club.mydlq.completable.DeadLockDemo.lambda$newThread$0(DeadLockDemo.java:24)
15        - waiting to lock <0x00000000fae02818> (a java.lang.Object)
16        - locked <0x00000000fae02828> (a java.lang.Object)
17        at club.mydlq.completable.DeadLockDemo$$Lambda$570/572868060.run(Unknown Source)
18        at java.lang.Thread.run(Thread.java:748)
19
20"ThreadA" #27 prio=5 os_prio=0 tid=0x00007f96510ec000 nid=0x4ea0 waiting for monitor entry [0x00007f9638376000]
21   java.lang.Thread.State: BLOCKED (on object monitor)
22        at club.mydlq.completable.DeadLockDemo.lambda$newThread$0(DeadLockDemo.java:24)
23        - waiting to lock <0x00000000fae02828> (a java.lang.Object)
24        - locked <0x00000000fae02818> (a java.lang.Object)
25        at club.mydlq.completable.DeadLockDemo$$Lambda$570/572868060.run(Unknown Source)
26        at java.lang.Thread.run(Thread.java:748)
27......

可以根据上面的结果观察到,ThreadB 中存在信息 java.lang.Thread.State: BLOCKED 和 ThreadA 中存在信息 java.lang.Thread.State: BLOCKED,这俩个线程目前处于锁的状态,可能发生了死锁,触发的类为 DeadLockDemo,因此我们重点排查该类中的代码逻辑,分析是否可能造成死锁即可。

3.5 示例5: 使用 jstack 检查高 CPU 线程

查找 Java 应用进程 ID

使用 jps 命令查找 Java 进程 ID,命令如下:

1$ jps -l
2
362640 app.jar
417032 Jps

查看 CPU 占用高的线程

使用 top 命令,查看指定进程中占用 CPU 高的线程,命令如下:

 1$ top -Hp 62640
 2
 3PID   USER   PR  NI  VIRT     RES     SHR    S  %CPU %MEM  TIME+    COMMAND
 462759 root   20  0   2994680  248360  13284  S  13.3  13.3  0:03.79  java
 562760 root   20  0   2994680  248360  13284  S  2.3  13.3  0:03.77  java
 662761 root   20  0   2994680  248360  13284  S  2.3  13.3  0:03.76  java
 762762 root   20  0   2994680  248360  13284  S  2.0  13.3  0:03.76  java
 862640 root   20  0   2994680  248360  13284  S  0.0  13.3  0:00.00  java
 962641 root   20  0   2994680  248360  13284  S  0.0  13.3  0:01.77  java
1062642 root   20  0   2994680  248360  13284  S  0.0  13.3  0:00.07  java
1162643 root   20  0   2994680  248360  13284  S  0.0  13.3  0:00.04  java

转换线程 ID 为 16 进制

使用 printf 命令将 62760 转换为 16 进制,命令如下:

1$ printf "%x\n" 62760
2
3f528

使用 jstack 查找指定线程信息

查找进程ID 62760 的应用中,线程ID f528 的信息,输出查找到的信息的后 30 行内容:

 1$ jstack 62640 | grep f528 -A 30
 2
 3"ThreadB" #28 prio=5 os_prio=0 tid=0x00007ff904fde800 nid=0xf528 waiting on condition [0x00007ff8afefd000]
 4   java.lang.Thread.State: TIMED_WAITING (sleeping)
 5        at java.lang.Thread.sleep(Native Method)
 6        at java.lang.Thread.sleep(Thread.java:340)
 7        at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
 8        at club.mydlq.completable.HighCpuUsageDemo.lambda$newThread$0(HighCpuUsageDemo.java:23)
 9        at club.mydlq.completable.HighCpuUsageDemo$$Lambda$570/1549725679.run(Unknown Source)
10        at java.lang.Thread.run(Thread.java:748)
11
12......

由上面信息可以看出,主要是由 ThreadB 线程,执行了 HighCpuUsageDemo 类,所以需要重点排查该类中的代码逻辑,分析哪些可能造成 CPU 飙升。

--- END ---


  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。