[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
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文件导出
- jinfo
- 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
- 阿里巴巴开源的Java 应用诊断利器
- https://arthas.aliyun.com/en/
- YourKit Java Profiler
- YourKit是一个商业化的Java性能分析工具,提供了线程转储分析功能,可以快速定位线程问题,同时还提供了内存和CPU分析功能。
- https://www.yourkit.com/java/profiler/
- FastThread
- FastThread是一款针对Java线程分析的工具,可以帮助用户分析线程转储文件,查找线程问题,识别性能瓶颈等。
- https://fastthread.io/
- Glowroot |
- Glowroot是一个开源的Java应用性能监控工具(Open source Java APM),可以生成线程转储文件,并提供了一些线程分析功能,如线程状态、锁信息等。
- https://glowroot.org/
- spotify thread dump analyzer
这些工具都提供了线程转储分析的功能,可以帮助开发人员定位线程问题和优化应用性能。具体选择哪个工具,可以根据自己的需求和喜好进行选择。
X 参考文献
- VisualVM
- https://github.com/oracle/visualvm
- https://visualvm.github.io/releases.html
- JDK1.8 推荐下载 : V 2.1.5 # 2022.10.18 Release
- Questions
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!