跳至主要內容

JVM 性能监控工具之命令行篇

沉默王二Java核心Java虚拟机约 4758 字大约 16 分钟

第十四节:JVM 性能监控之命令行篇

记得 2014 年我在写大宗期货交易平台的时候,遇到了一些棘手的问题,可能是因为我的并发编程知识掌握的不够扎实,导致出现了内存泄漏的问题。

当时排查了好久,用的工具就是 JDK 自带的 jconsole,之前也没有过类似的性能监控经验,就导致在查找问题的时候非常痛苦,至今印象深刻。

那今天我们就从工具篇出发,来看看这些命令行工具的具体使用方法,以及如何排查问题。

JDK 性能监控工具

除了我们的老朋友 java 和 javac 命令,在 Java 的 bin 目录下,还有很多其他的命令行工具,比如说用于性能监控的 jps、jstat、jinfo、jmap、jstack、jcmd 等等。

我用的 macOS,Windows 用户看到的可能是带有 .exe 结尾的,但是功能都是一样的,我就不再刻意去截图了。

接下来,我来给大家一一介绍一下这些工具的用途,认个脸熟。

jps:查看虚拟机进程

jps(Java Virtual Machine Process Status Tool)类似 Linux 下的 ps,用于快速查看哪些 Java 应用正在运行,以及它们的进程 ID,这对于进一步使用其他 JVM 工具进行诊断是必要的。

jps 命令格式:

jps [ options ] [ hostid ] 

jps 命令示例:

①、注意看第三个进程正是我本地运行着的技术派open in new window实战项目,一个前后端分离的 Spring Boot+React 的社区项目,帮助不少球友拿到了心仪的校招 Offer。

②、pid 是什么?pid 是进程 ID,是操作系统分配给进程的唯一标识符,可以用来查看进程的详细信息。

通常情况下,我们关闭一个进程可以通过右上角的 X 号来完成,但有了 pid,我们可以直接在命令行通过 kill 命令来关闭进程,比如:

kill -9 pid

意思是强制关闭 pid 对应的进程,新手可千万别在生产环境下乱 kill 哈(😂)。

再来看一下 jps 的常用选项:

选项列表描述
-q只输出进程 ID,忽略主类信息
-l输出主类全名,或者执行 JAR 包则输出路径
-m输出虚拟机进程启动时传递给主类 main() 方法的参数
-v输出虚拟机进程启动时的 JVM 参数

jstat:查看 JVM 运行时信息

jstat(Java Virtual Machine Statistics Monitoring Tool)用于监控 JVM 的各种运行时状态open in new window信息,提供有关垃圾回收、类加载、JIT 编译等运行数据。

jstat 命令格式为:

jstat [ option vmid [interval[s|ms] [count]] ]

选项 option 主要分为三类:类加载、垃圾收集、运行期编译状况。

①、-class:监视类装载、卸载数量、总空间以及类装载所耗费的时间。

如下命令 jstat -class -t 75952 1000 2 会输出进程 75952 的类装载信息,每秒统计一次,一共输出两次。

  • Loaded:加载的类的数量。
  • Bytes:所有加载类占用的空间大小。
  • Unloaded:卸载的类的数量。
  • Time:类加载器所花费的时间。

②、-gc:监视 Java 堆open in new window状况,包括 Eden 区、2 个 Survivor 区、老年代等容量、已用空间、GC 时间合计等信息。

如下命令 jstat -gc 75952 1000 2 会输出进程 75952 的 GC 信息,每秒统计一次,一共输出两次。结果比较多,我就截断折叠了一下,方便大家查看。

  • S0C, S1C, S0U, S1U:Survivor 区的大小和使用情况,一个 From 一个 To,C 为当前大小(Current),U 为已使用大小(Used)。
  • EC, EU:Eden 区的大小和使用情况。
  • OC, OU:老年代(Old)的大小和使用情况。
  • MC, MU:元空间(Metaspace)的大小和使用情况。
  • GC,GCC:GC 表示垃圾回收器进行 Minor GC(年轻代垃圾回收)的累计次数和总时间;GCC 表示垃圾回收器进行 Major GC(老年代垃圾回收,也称为 Full GC)的累计次数和总时间。

③、-compiler:监视 JIT 编译器编译过的方法、耗时等信息。

  • Compiled:编译的方法数量。
  • Failed:编译失败的方法数量。
  • Invalid:失效的编译方法数量。
  • Time:编译所花费的时间。

如下命令 jstat -compiler 75952 1000 2 会输出进程 75952 的编译信息,每秒统计一次,一共输出两次。

好,我们再来总结一下 jstat 的主要选项,见下表:

选项列表描述
-class监视类加载、卸载数量、总空间以及类装载所耗费时长
-gc监视 Java 堆情况,包括 Eden 区、2 个 Survivor 区、老年代、元空间等,容量、已用空间、垃圾收集时间合计等信息
-gccapacity监视内容与-gc 基本一致,但输出主要关注 Java 堆各个区域使用到的最大、最小空间
-gcutil监视内容与-gc 基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause与 -gcutil 功能一样,但是会额外输出导致上一次垃圾收集产生的原因
-gcnew监视新生代垃圾收集情况
-gcnewcapacity监视内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间
-gcold监视老年代垃圾收集情况
-gcoldcapacity监视内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间
-compiler输出即时编译器编译过的方法、耗时等信息
-printcompilation输出已经被即时编译的方法

jinfo:查看虚拟机配置

jinfo(Configuration Info for Java)用于在补重启应用的情况下,调整虚拟机的各项参数,或者输出 Java 进程的详细信息。

jinfo 命令格式:

jinfo [ option ] pid

如下命令 jinfo -flags 88952 会输出进程 88952 的 JVM 参数信息。

不过很遗憾的是,我的 macOS 系统上,jinfo 命令无法执行成功,后来经过各种实验找到了解决办法。

可能的原因是,我的 macOS 上装了太多的 JDK 版本,导致 Intellij IDEA 中编译的 JDK 和 jinfo 的版本不一致。

那怎么解决呢?

尝试方案 1:用相同的 JDK 版本编译运行 Java 程序,并使用相同的 JDK 的 jinfo 来查看。

结果依然报错,可能的原因是 JDK 版本过旧。

尝试方案 2: 用 JDK 11 来测试,代码用 JDK 11 编译和运行。

然后用 JDK 11 的 jinfo 来查看,成功了。

再试一下 jinfo -flags 10025 命令,也 OK。

之所以把这个问题的解决思路同步上来,也是希望能给球友们open in new window提供一些日常遇到开发问题时的解决思路。

jmap:导出堆快照

jmap 命令用于生成堆转储快照(一般称为 heap dump 或 dump 文件)。堆转储包含了 JVM 堆open in new window中所有对象的信息,包括类、属性、引用等。这对于分析内存泄漏和优化内存使用非常有帮助。

当然了,jmap 的作用不局限于此,它还可以查看堆的空间使用率、当前用的是哪种垃圾收集器open in new window等。

jmap 命令格式:

jmap [ option ] vmid

如下命令 jmap -histo 10025 会输出进程 10025 的堆内存中所有对象的数量和占用内存大小的汇总信息,按照内存使用量排序。

如下命令 jmap -dump:format=b,file=heap.hprof 10025 会输出进程 10025 的堆快照信息,保存到文件 heap.hprof 中。

简单解释一下这条命令:

  • format:文件格式,这里是 b,表示二进制格式。
  • file:文件名。

那么,我们可以用什么工具来打开这个文件呢?后面会讲。我们先来看一下 jmap 的主要选项:

选项描述
-dump生成 Java 堆转储快照。
-finalizerinfo显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象。Linux 平台
-heap显示 Java 堆详细信息,比如:用了哪种回收器、参数配置、分代情况。Linux 平台
-histo显示堆中对象统计信息,包括类、实例数量、合计容量
-F当虚拟机进程对 -dump 选项没有响应式,可以强制生成快照。Linux 平台

jstack:跟踪Java堆栈

jstack 用于打印出 JVM 中某个进程或远程调试服务的线程堆栈信息(一般称为 threaddump 或者 javacore 文件)。它常用于诊断应用程序中的线程问题,比如线程死锁、死循环或长时间等待。

jstack 命令格式:

jstack [ option ] vmid

如下 jstack -l 10025 会输出进程 10025 的线程堆栈信息,包括锁信息。

jstack 工具主要选项:

选项描述
-F当正常输出的请求不被响应时,强制输出线程堆栈
-l除了堆栈外,显示关于锁的附加信息
-m如果调用的是本地方法的话,可以显示 c/c++的堆栈

我们来通过一个线程死锁的问题,来看一下 jstack 的使用方法。

首先,我们编写一个死锁的程序:

class DeadLockDemo {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock1) {
                System.out.println("线程1获取到了锁1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("线程1获取到了锁2");
                }
            }
        }).start();

        new Thread(() -> {
            synchronized (lock2) {
                System.out.println("线程2获取到了锁2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("线程2获取到了锁1");
                }
            }
        }).start();
    }
}

我们创建了两个线程,每个线程都试图按照不同的顺序获取两个锁(lock1 和 lock2)open in new window。这种锁的获取顺序不一致很容易导致死锁。

运行这段代码,果然卡住了。

运行 jstack pid 命令,可以看到死锁的线程信息。诚不欺我!

jcmd:多功能命令

jcmd 是一个多功能命令,可以用于收集堆转储、生成 JVM 和 Java 应用程序的性能数据,以及动态更改某些 Java 运行时参数。jcmd 提供的功能比其他单一命令,如 jstack, jmap, jstat 都要强大。

例如,使用 jcmd -l 列出当前的所有 Java 应用,和 jps 类似:

例如,使用 jcmd 10025 help 查看进程 10025 支持的命令:

例如,使用 jcmd 10025 VM.flags 查看进程 10025 的 JVM 参数,相当于 jinfo -flags 10025

例如,使用 jcmd 10025 Thread.print 查看进程 10025 的线程信息,相当于 jstack 10025

jmcd 命令格式:

 jcmd <pid | main class> <command ... | PerfCounter.print | -f  file>

jmcd 的主要选项:

选项描述补充
help打印帮助信息,示例:jcmd help []
ManagementAgent.stop停止 JMX Agent
ManagementAgent.start_local开启本地 JMX Agent
ManagementAgent.start开启 JMX Agent
Thread.print参数-l 打印 java.util.concurrent 锁信息,相当于:jstack
PerfCounter.print相当于:jstat -J-Djstat.showUnsupported=true -snap
GC.class_histogram相当于:jmap -histo
GC.heap_dump相当于:jmap -dump:format=b,file=xxx.bin
GC.run_finalization相当于:System.runFinalization()
GC.run相当于:System.gc()
VM.uptime参数-date 打印当前时间,VM 启动到现在的时候,以秒为单位显示
VM.flags参数-all 输出全部,相当于:jinfo -flags , jinfo -flag
VM.system_properties相当于:jinfo -sysprops
VM.command_line相当于:jinfo -syspropsgrep command
VM.version相当于:jinfo -syspropsgrep version

操作系统工具

除了 JDK 自带的命令行,我们很多时候还要使用操作系统为我们提供的命令行工具,来完成性能监控的监测。

比如说 top、vmstat、iostat、netstat 等等。

top:显示系统整体资源使用情况

top 命令用于实时显示系统中各个进程的资源占用情况,如 CPU 和内存使用率。常用于快速查看哪些进程占用了较高的资源。

该命令的输出结果是实时变化的,可以使用 ctrl + c 来退出。下图是我的 macOS 上输出的结果:

top 命令的输出可以分为两个部分:前半部分是系统统计信息,后半部分是进程信息。

统计信息

统计信息是针对整个系统的,主要包括系统负载、CPU 使用率、内存使用情况、虚拟内存使用情况、网络和硬盘使用情况等。

  • 第 1 行是进程和线程信息,分别表示总进程数、正在运行的进程数、睡眠的进程数、线程数。
  • 第 2 行是负载均衡和 CPU 使用率信息,Load Avg: 4.02, 3.89, 3.29:这表示过去 1 分钟、5 分钟和 15 分钟的平均系统负载。负载大于 3 意味着系统相对繁忙;CPU usage: 6.97% user, 3.54% sys, 89.47% idle:用户占用了 6.97% 的 CPU,系统占用了 3.54%,还有 89.47% 的 CPU 处于空闲。
  • 第 3 行是共享库(Shared Libraries)内存使用的信息。这一行的数据主要涉及到操作系统加载的共享库(如动态链接库或共享对象文件)。
  • 第 4 行是内存区域(Memory Regions)的使用信息。内存区域是指操作系统为应用程序和进程分配的内存块。每个内存区域都有特定的用途和属性,比如代码、数据、堆、栈等。这一行的数据提供了系统内存使用的更详细的视图。
  • 第 5 行是内存使用情况,PhysMem: 30G used (3018M wired), 1547M unused:内存总共使用了 30GB,还有大约 1547MB 的内存未使用;
  • 第 6 行是虚拟内存的信息,虚拟内存是计算机内存管理的一种技术,它为每个程序提供一种“虚拟”的地址空间,这些地址空间对于每个程序来说都是连续的,但实际上可能分散在物理内存和磁盘的交换空间(swap space)上。
  • 第 7 行是网络和硬盘信息,Networks: packets: 22655692/19G in, 19180791/11G out:网络接收了 19GB 的数据包;发送了约 11GB 的数据包;Disks: 14866544/288G read, 15176739/251G written:硬盘读取 14866544 次;写入了约 15176739 次。

进程信息

在进程信息区中,显示了系统各个进程的资源使用情况。主要字段的含义:

  • PID:进程 id
  • COMMAND:命令名/命令行
  • %CPU:进程占用的 CPU 使用率
  • TIME:进程使用的 CPU 时间总计,单位 1/100 秒
  • MEM:进程使用的物理内存和虚拟内存大小,单位 KB

Windows 用户可以使用 tasklist 命令来查看进程信息。

vmstat:监控内存和 CPU

vmstat 是 Linux 上的一款功能比较齐全的性能监测工具。它可以统计 CPU、内存、swap 的使用情况。

一般 vmstat 工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数,单位是秒,第二个参数是采样的次数,如:

vmstat 1 3 命令表示每秒采样一次,共三次。

输出的各个列的含义:

vmstat 的用法如下:

vmstat [options] [delay [count]]

vmstat 的主要选项:

  • [options]:提供不同的输出选项,例如 -a 显示活跃和非活跃内存,-d 显示磁盘统计,-s 显示内存统计等。
  • [delay]:在连续模式下,两次报告之间的延迟时间(秒)。
  • [count]:要显示的报告数量。

iostat:监控 IO 使用

iostat 用于统计 CPU 使用信息和磁盘的 IO 信息。

基本用法如下:

iostat [options] [interval [count]]
  • [options]:提供不同的输出选项。例如,-c 显示 CPU 使用情况,-d 显示磁盘使用情况,-x 显示扩展统计信息等。
  • [interval]:报告之间的延迟时间(秒)。
  • [count]:显示报告的次数。

iostat 的输出包括两个主要部分:

  1. CPU 使用情况

    • user:用户程序使用的 CPU 时间百分比。
    • system:系统(内核)级程序使用的 CPU 时间百分比。
    • idle:CPU 空闲时间百分比。
  2. 磁盘 I/O 统计

    • tps(Transfers Per Second):每秒传输次数。
    • kB_read/s:每秒读取的千字节数。
    • kB_wrtn/s:每秒写入的千字节数。
    • kB_readkB_wrtn:分别是读取和写入的总千字节数。

    如果使用 -x 选项,会显示更详细的统计信息,例如:

    • %util:表示磁盘的繁忙程度。
    • await:I/O 请求的平均等待时间(毫秒)。
    • svctm:服务时间,即完成一个 I/O 请求所需的平均时间。

使用示例如下:

①、查看 CPU 和所有磁盘设备的基本 I/O 统计信息:

iostat

②、查看磁盘 I/O 统计信息,每 2 秒更新一次:

iostat -dx 2

③、只查看 CPU 使用情况:

iostat -c

netstat:监控网络使用

netstat(network statistics)用于监控和显示网络相关信息。基本用法如下:

netstat [options]
  • [options]:提供不同的输出选项。常见的选项包括 -a(显示所有连接和侦听端口),-t(显示 TCP 连接),-u(显示 UDP 连接),-n(以数字形式显示地址和端口号),-r(显示路由表)等。

netstat 的输出通常包括以下几个方面的信息:

①、网络连接:显示活动的或监听的套接字连接,包括服务名、本地地址和端口、远程地址和端口、连接状态等。

②、路由表:显示网络路由表,包括目的地址、网关、子网掩码、使用的接口等。

小结

今天我们介绍了 JDK 自带的性能监控工具,以及操作系统提供的一些命令行工具。

这些工具在排查问题时非常有用,希望大家一定要掌握,以备不时之需。

参考


GitHub 上标星 10000+ 的开源知识库《二哥的 Java 进阶之路open in new window》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 10000+ 的 Java 教程open in new window

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。