[JVM/APM] 应用诊断工具之VisualVM

1 概述

1.1 简介

VisualVM is a visual tool integrating commandline JDK tools and lightweight profiling capabilities. See https://visualvm.github.io for details, downloads and documentation.
VisualVM 是一款集成了命令行JDK工具和轻量化分析能力的可视化工具。
详情、下载、文档请参见 https://visualvm.github.io

1.2 获取本工具

Use Apache Ant 1.9.9 or above and Oracle JDK 8 to build VisualVM from this repository.

1.3 获取源码

First download or clone this repository into directory visualvm. There are two project suites included:首先下载或克隆这个仓库(https://github.com/oracle/visualvm)到visualvm目录下。这里包含有2个项目套件:

  • visualvm (visualvm/visualvm) - suite for the core VisualVM tool
  • plugins (visualvm/plugins) - suite for the VisualVM plugins available in Plugins Center

1.4 如何运行

1.4.1 ant 方式

To run VisualVM, use ant run command in the visualvm/visualvm directory.

1.4.2 windows cmd 方式

本节以 Windows 10、JDK=1.8 ,VisualVM Version = 2.1.5 为例。

  • Step1 编辑${VisualVM_HOME}/etc/visualvm.conf文件,设置JDK路径
#visualvm_jdkhome="/path/to/jdk"
visualvm_jdkhome="D:\Program\Java\jdk1.8.0_261"

此步骤是为了防止启动失败、并报错Connot find Java 1.8 or higher

  • Step2 双击${VisualVM_HOME}\bin\visualvm.exe启动

  • Step3 安装所需插件 | 可选步骤

菜单路径:Tools > Plugins

  • Step4 调试应用程序 或 heap / thread dump 文件

Step4.1 以导入 thread dump 文件为例:

Step4.2 以监听某个运行中的应用为例:

注:启动后运行过程中的软件数据存放于C:\Users\${USER}\AppData\Roaming\VisualVM,此目录可按自己需要进行重置、清理。

2 JVM 线程分析基础

2.1 Java线程状态

在Java中,线程的状态主要分为以下几种:

  • 新建(New):线程被创建时的状态。
  • 就绪(Runnable):当线程已经被启动并且没有任何阻止它立即运行的条件时,线程处于这种状态。此时,线程在等待CPU分配时间片,即等待操作系统调度。
  • 运行(Running):线程获得CPU资源并执行其代码。
  • 阻塞(Blocked):当线程等待某些资源或满足某些条件(例如,等待I/O操作完成)时,它进入阻塞状态。
  • 等待(Waiting):线程进入等待状态,直到另一个线程执行特定的操作(如通知或中断)。
  • 超时等待(Timed Waiting):这是在一定时间内进入等待状态的线程的状态。
  • 终止(Terminated):当线程已经执行完毕或者异常结束时,它处于这种状态。

2.2 最佳实践

thread dump 文件里,值得关注的线程状态有:

  • 死锁, Deadlock(重点关注)
  • 执行中,Runnable
  • 等待资源, Waiting on condition(重点关注)
  • 等待获取监视器, Waiting on monitor entry(重点关注)
  • 暂停,Suspended
  • 对象等待中,Object.wait() 或 TIMED\_WAITING
  • 阻塞, Blocked(重点关注)
  • 停止,Parked

详见:[JVM] Java Thread Dump 分析 - 博客园/千千寰宇

2.3 案例

2.3.1 案例:网约车场景业务代码讲解线程状态的转变过程

以下是根据网约车场景编写的完整的业务代码,来讲解线程状态的转变过程:

public class Order {
  private boolean isConfirmed;

  public synchronized void confirmOrder() {
    while (!isConfirmed) {
      try {
        wait(); // 等待订单被确认
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  public synchronized void confirm() {
    isConfirmed = true;
    notifyAll(); // 通知所有等待的线程订单已被确认
  }
}

public class Driver {
  private Order order;

  public Driver(Order order) {
    this.order = order;
  }

  public void confirmOrder() {
    order.confirmOrder(); // 等待订单被确认
  }
}

public class Passenger {
  private Order order;

  public Passenger(Order order) {
    this.order = order;
  }

  public void confirmOrder() {
    order.confirm(); // 确认订单
  }
}

public class Main {
  public static void main(String[] args) {
    Order order = new Order();
    Driver driver = new Driver(order);
    Passenger passenger = new Passenger(order);

    Thread driverThread = new Thread(() -> {
      driver.confirmOrder();
    });

    Thread passengerThread = new Thread(() -> {
      try {
        Thread.sleep(1000); // 模拟乘客确认订单的时间延迟
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      passenger.confirmOrder();
    });

    driverThread.start();
    passengerThread.start();
  }
}

在这个例子中,司机端线程和乘客线程都访问同一个订单对象。当司机端线程尝试获取乘客订单时,它必须先获取这个订单的锁,因此它处于monitor状态。当乘客确认订单后,司机端线程会收到通知,并继续执行代码,因此它会从wait状态转变为running状态。在这个过程中,乘客线程会先进入sleeping状态(模拟乘客确认订单的时间延迟),然后在确认订单后进入running状态。

让我们结合以上代码来说明线程状态是如何转换的:

  • 新建(New):一个线程在它被创建时处于这种状态。在您的代码中,当您创建一个新的线程实例(如Thread driverThread = new Thread(() -> {...});)时,线程处于新建状态。
  • 就绪(Runnable):当线程已经被启动并且没有任何阻止它立即运行的条件时,线程处于这种状态。在您的代码中,当driverThread.start();和passengerThread.start();被调用时,两个线程进入就绪状态,等待CPU分配时间片。
  • 运行(Running):线程获得CPU资源并执行其代码。在您的代码中,当driver.confirmOrder();和passenger.confirmOrder();被执行时,线程正在运行。
  • 阻塞(Blocked):当线程等待某些资源或满足某些条件(例如,等待I/O操作完成)时,它进入阻塞状态。在您的代码中,当driver.confirmOrder();在循环中调用wait()方法时,它会释放对象的锁并进入等待(Waiting)状态。注意,这里不是阻塞状态,因为wait()方法是由当前线程主动调用的。
  • 等待(Waiting):线程进入等待状态,直到另一个线程执行特定的操作(如通知或中断)。在您的代码中,当driver.confirmOrder();调用wait()方法时,它会进入等待状态,直到passenger.confirmOrder();调用notifyAll()方法。
  • 超时等待(Timed Waiting):这是在一定时间内进入等待状态的线程的状态。在您的代码中,没有体现这种状态。
  • 终止(Terminated):当线程已经执行完毕或者异常结束时,它处于这种状态。在您的代码中,当driver.confirmOrder();和passenger.confirmOrder();执行完毕时,两个线程将结束并处于终止状态。

需要注意的是,线程状态的转换可能会受到多种因素的影响,例如线程的优先级、CPU的调度策略等。此外,Java中的线程状态转换并不是完全独立的,多个状态之间可能存在重叠交互

3 VisualVM 线程分析

3.1 线程状态

在JVisualVM的线程面板中,你可以看到以下线程状态

  • monitor:对应Java线程的"阻塞"状态。这表示线程正在等待进入某个同步块,但还没有成功获取到所需的锁。
    • Monitor:线程正在监视一个对象,等待获取该对象的monitor(即对象的锁)。
    • 例如,当线程需要访问共享资源时,它必须先获取该资源的锁。在网约车系统中,假设有一个“共享资源”是乘客的订单信息。当司机端线程尝试获取乘客订单时,它必须先获取这个订单的锁。
synchronized(order) {
  // 司机端线程处于monitor状态,获取乘客订单信息
}
  • running:对应Java线程的"就绪"和"运行"状态。这表示线程正在执行或者已经准备好执行,只需等待CPU时间片。
    • Running:线程正在执行代码并且没有被阻塞。
    • 例如,在网约车系统中,当司机端线程获取到乘客订单后,开始导航去接乘客,这个时候司机端线程处于running状态。
while(driver.hasOrder) {
  // 司机端线程处于running状态,导航去接乘客
}
  • sleeping:对应Java线程的"超时等待"状态。这表示线程在一段时间内无法获得所需资源,例如Thread.sleep()或者Object.wait(long timeout)的调用。
    • Sleeping:线程暂时不会执行任何代码,直到被唤醒。在网约车系统中,假设有一个“空闲时间”的概念,即司机在没有订单的时候,线程会进入sleeping状态,等待新的订单。
try {
  Thread.sleep(idleTime); // 司机端线程处于sleeping状态,等待新的订单
} catch (InterruptedException e) {
  e.printStackTrace();
}
  • wait:对应Java线程的"等待"状态。这表示线程正在等待某个特定的条件成立,例如等待某个锁的释放。
    • Wait:线程正在等待一个特定的条件变为真,或者等待另一个线程执行某个特定的操作。
    • 例如,在网约车系统中,当司机完成订单后,需要等待乘客确认订单完成。这个时候司机端线程就处于wait状态。
synchronized(order) {
  while(!order.isConfirmed) {
    order.wait(); // 司机端线程处于wait状态,等待乘客确认订单完成
  }
}
  • park:对应Java线程的"超时等待"状态。这通常表示线程正在等待一个特定的对象或者锁,但如果在指定的时间内没有获得,则线程将被唤醒并继续执行。
    • Park:线程不会执行任何代码,直到被取消暂停。
    • 例如,在网约车系统中,假设有一种“暂停服务”的情况,即司机在完成订单后选择暂停服务,这个时候司机端线程就处于park状态。
LockSupport.park(); // 司机端线程处于park状态,暂停服务

在Java中,线程的状态转换从wait状态先变为runnable状态,然后再获得CPU时间片(或者说竞争到CPU执行权),最终转变为running状态。

3.2 线程堆栈

从JVisualVM dump线程的堆栈情况来看,你可以看到以下线程状态::

  • Runnable:对应Java线程的"就绪"和"运行"状态。这表示线程正在执行或者已经准备好执行,只需等待CPU时间片。
  • Waiting on condition:对应Java线程的"等待"状态。这表示线程正在等待某个特定的条件成立,例如等待某个锁的释放。
  • Waiting on monitor entry:对应Java线程的"阻塞"状态。这表示线程正在等待进入某个同步块,但还没有成功获取到所需的锁。
  • Parking to wait for <0x000000079ae48820> (a java.util.concurrent.locks.ReentrantLock$NonfairSync):这也是一种"等待"状态,表示线程正在等待一个特定的对象或者锁。
  • TIMED_WAITING:对应Java线程的"超时等待"状态。这表示线程在一段时间内无法获得所需资源,例如Thread.sleep()或者Object.wait(long timeout)的调用。
  • TERMINATED:对应Java线程的"终止"状态。这表示线程已经完成执行或者由于异常而结束。

注意,JVisualVM的线程状态和Java的线程状态并不是完全一一对应的,有的JVisualVM线程状态可能包含了Java线程的多个状态。

在jvisualvm的性能监控面板中,我们可以看到应用程序的CPU和内存使用情况。如果应用程序的CPU使用率很高,那么说明它正在处理大量的请求或者进行一些计算密集型的操作。如果应用程序的内存使用率很高,那么说明它正在使用大量的内存来处理请求或者存储数据。

4 VisualVM的重要插件

博主已安装过的插件:

  • VisualVM-TDA-Module

TDA = Thread Dump Analysis
TDA是ThreadDumpAnalyzer的缩写,是一款线程快照分析工具
当使用jstack或者VisualVM等工具取得线程快照文件后,通过文本编辑器查看和分析线程快照文件是一件非常艰难的事情。而TDA的功能就在 于帮助开发者分析导出的线程快照。
TDA可以在 http://java.net/projects/tda/上下载。

5 JVM 线程分析工具

  • 操作系统
    • kill -3 {pid}
      • kill -3 是一个 Unix/Linux 系统中的命令,用于向进程发送一个 SIGQUIT 信号。SIGQUIT 信号通常用于请求进程进行核心转储(dump core),以便进行调试和分析。
      • 当你在终端中运行 kill -3 <进程ID> 时,会向指定进程发送 SIGQUIT 信号,进程会收到该信号并执行相应的操作。通常情况下,进程会生成一个核心转储文件,其中包含了进程在发生错误或异常时的内存和寄存器状态。这个核心转储文件可以用于后续的调试和分析。
      • 注意:kill -3 命令只是向进程发送信号,具体的操作和响应取决于进程的实现。不同的进程可能对 SIGQUIT 信号有不同的处理方式,有些进程可能会忽略该信号,而有些进程可能会执行特定的操作。在使用 kill -3 命令时,请确保你有足够的权限来发送信号给指定的进程,并且谨慎使用,以免对系统和进程造成不可预料的影响。
      • Java 应用程序
        • 如果项目通过 Tomcat 进行发布,即普通的 web 项目,则对应的堆栈信息会打印在 catalina.out 文件中。
        • 如果项目是基于 SpringBoot 并且使用 nohup java -jar xxx.jar & 命令运行,则 java 堆栈信息会在 jar 包所在的 nohup.out 文件中。
  • JDK
    • jinfo
      • 样例命令:jinfo {PID}
        • 打印 JVM 的各类参数(Java System Properties、VM Flags、VM Arguments)
    • jcmd
      • 支持版本:JDK 1.7 +
      • Java HotSpot 虚拟机的 NMT 功能
      • 样例命令:
        • jcmd {PID} help | 列出当前运行的 java 进程可以执行的操作
        • jcmd {PID} Thread.print > C:\Users\{USER}\Desktop\thread-dump-{PID}.tdump
        • jcmd {PID} VM.flags
        • jcmd {PID} VM.system_properties
        • jcmd {PID} VM.command_line
    • jstack
      • jstack是JDK自带的一个命令行工具,用于生成Java线程转储文件。它可以显示线程状态,锁信息,监视死锁等。
      • 样例命令:jstack [-l][F] pid | 可进行的dump文件导出
  • VisualVM
    • VisualVM是一个功能强大的Java应用程序性能分析工具、是NetBeans的profile子项目。
    • 其可以生成线程转储文件,同时还提供了堆转储、CPU和内存分析等功能。
    • 在线程面板中,monitor、running、sleeping、wait、park分别代表线程的不同状态。
    • https://github.com/oracle/visualvm
    • https://visualvm.github.io/releases.html
  • Eclipse MAT
    • Eclipse MAT(Memory Analyzer Tool)是一个专业的Java内存分析工具,可以分析堆转储文件,查找内存泄漏问题,分析对象引用关系等。
    • https://eclipse.dev/mat/
  • Arthas
  • YourKit Java Profiler
    • YourKit是一个商业化的Java性能分析工具,提供了线程转储分析功能,可以快速定位线程问题,同时还提供了内存和CPU分析功能。
    • https://www.yourkit.com/java/profiler/
  • FastThread
    • FastThread是一款针对Java线程分析的工具,可以帮助用户分析线程转储文件,查找线程问题,识别性能瓶颈等。
    • https://fastthread.io/


这些工具都提供了线程转储分析的功能,可以帮助开发人员定位线程问题和优化应用性能。具体选择哪个工具,可以根据自己的需求和喜好进行选择。

X 参考文献

posted @ 2023-12-06 16:36  千千寰宇  阅读(156)  评论(0编辑  收藏  举报