上一章我们介绍了如果使用JDK内置的一些命令,去分析、优化以及帮助我们解决应用程序中的一些问题。确实那些命令虽然使用起来十分简单,但是我们也能感受到其功能的强大。不过由于其采用命令行的特性,在一定的程度上也提升了我们的阅读和使用门槛。
这一章我们就来介绍两款JVM的可视化工具,更直观的可视化界面对我们分析程序可以说是事半功倍。
JDK除了提供了大量命令行工具外,还内置了两个强大的可视化工具:
JConsole
、VisualVM
。
JConsole (Java Monitoring and Management Console)
是一款基于JMX的可视化监控、管理工具,简单来说就是我们可以通过它来监控和优化Java应用程序性能。下面我们就来看下它和我们之前使用的命令行工具差别到底在哪里。
使用方式很简单,我们先把之前
springboot-jvm
项目启动好,这里示例代码地址在第一章头部会贴出,大家也可以通过官网或者自己去构建一个简单的程序跑起来就ok了。
我们打开JDK安装目录,在bin
目录下有一个jconsole
应用程序,点击即可启动。或者大家配置好了JAVA_HOME
的话在cmd命令行直接输入jconsole
也是一样的效果。另外官网也提供了JConsole的使用方法【Using Jconsole - Java SE Monitoring and Management Guide】,大家有兴趣可以去了解一哈。
这里可以看到它会自动搜索我们本机的所有虚拟机进程,不需要和我们之前使用命令行一样先自己去使用jps
找到进程对应pid,这里我们直接选择我们需要监控的进程即可。
连接成功后,我们可以看到整个界面十分简洁明了。看得出来JConsole主要提供这几方面的监控:内存、线程、类、VM概要、MBean。其中概述会把每个模块大致情况反映出来,比如堆使用情况、线程使用情况、类加载情况以及CPU占有情况。
另外这里提一下,这里每张图标我们通过鼠标右键导出数据到CSV的,这里我们有需要可以通过导出之后用其他方式对其进行特定分析。接下来我们来对每个模块做一个简单的了解。
我们可以看到内存界面,相当于我们之前的
jstat
命令,主要用于监控虚拟机内存情况。
这些数据其实对于分析Java内存问题或者调优都是十分有价值的,我们可以查看堆内存、非堆内存、各分代的内存分配使用情况以及GC相关信息。
我们可以调整使用不同的GC以及JVM参数,通过观察以得到最适合应用程序的参数组合,提升性能。
我们之前已经修炼了内存分配以及回收策略的功法,这里我们借着这个机会来亲身感受一下内存分配和回收在JVM中真实的情况。
package com.ithzk.springbootjvm.memoryallocation;
import java.util.ArrayList;
/**
* @ Description : jconsole memory
* @ Author : zekunhu
* @ CreateDate : 2020/4/28 16:21
* @ UpdateUser : zekunhu
* @ UpdateDate : 2020/4/28 16:21
* @ UpdateRemark :
* @ Version : 1.0
*/
public class JconsoleMemoryAllocation {
public byte[] bytes = new byte[512 * 1024];
public static void main(String[] args) throws InterruptedException {
System.out.println("JconsoleMemoryAllocation main thread start...");
Thread.sleep(15000L);
allocation(6000);
}
private static void allocation(int size){
ArrayList<JconsoleMemoryAllocation> objects = new ArrayList<>();
for(int i = 0; i < size ; i++){
try {
Thread.sleep(100L);
objects.add(new JconsoleMemoryAllocation());
System.out.println("allocation current: " + i);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
这里我们写了一个很简单的示例,主线程在睡眠一段时间后开始进行内存分配操作,每次构建一个对象都会进行至少512kb内存空间的分配,我们来观察一下内存使用情况的变化。
首先这里我们可以看到由于对象列表一直不会被收回,所以整个堆内存空间使用分配的增长会和时间成一个正比,这个不难理解。
我们再来看下Eden
区和Survivor
区,我们都知道当对象在Eden
区和Survivor From
区中进行分配时,如果分配达到某些条件或阈值后会放到Survivor To
区、Old Space
中,所以呈现一个折线的情况。
最后就是老年代了,这里和整个堆空间内存分配情况差不多,主要是因为新生代中的对象晋升到了老年代中。这里我们通过可视化程序更贴切感受了一把JVM内存分配流程,想必大家对此也会印象深刻。
线程界面这里,相当于我们之前的
jstack
命令,主要用于监控线程情况。
这里我们可以看到活跃线程数及其峰值,以及左下角可以看到每个线程的运行状态,包括阻塞以及等待次数都有记录。这里最下面还有一个检测死锁
的按钮,这里我图可能不太完善,后面的图大家会清晰看到。这里我们去编写一个示例来模拟几种线程常见的状态。
package com.ithzk.springbootjvm.memoryallocation;
public class JconsoleDeadLock {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
private static Object waiting = new Object();
private static Object timedWaiting = new Object();
public static void main(String[] args) throws InterruptedException {
Thread.sleep(15000L);
new Thread(() -> {
synchronized (lock1){
try {
System.out.println(Thread.currentThread().getName() + "get lock1");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "get lock2");
}
}
}, "线程1").start();
new Thread(() -> {
synchronized (lock2){
try {
System.out.println(Thread.currentThread().getName() + "get lock2");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
synchronized (lock1){
System.out.println(Thread.currentThread().getName() + "get lock1");
}
}
}, "线程2").start();
new Thread(() -> {
synchronized (waiting){
try {
waiting.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程3").start();
new Thread(() -> {
synchronized (timedWaiting){
try {
timedWaiting.wait(60000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程4").start();
}
}
这个是我们之前模拟死锁的示例,我们在里面另外加了两个线程用于模拟
WAITING
和TIMED_WAITING
状态,我们通过JConsole
看看是不是和我们所预料的情况一样。
我们启动后通过JConsole
查看我们自定义的几个线程,线程1和线程2都处于BLOCKED
状态,而线程3处于WAITING
、线程4处于TIMED_WAITING
状态,这和我们所预想的情况是一致的。这里我们点击检测死锁
或者线程旁边死锁页签。
这里可以清楚地看到线程1和线程2都处于死锁状态,并且可以看到所需资源被哪个线程占有。这里我们正好回顾一下线程长时间停顿的几种主要原因:等待外部资源(数据库连接、网络、设备等资源)、死循环、锁等待。
有了JConsole
对线程的可视化界面分析,让我们对线程运行的情况了解确实更方便、更高效。
这个界面主要就是用来监控类装载和卸载的情况,使用的话还是比较少的,不细讲。
VM概要主要就是展示当前应用程序所使用的VM的一些相关配置,例如JVM版本、运行时间、使用何种垃圾收集器以及使用VM参数等等。大家有兴趣可以自己看看,就是一个VM相关信息的概况。
这个界面主要就是展示MBean的一些相关信息。关于MBean我这边并不是特别了解,查阅了一些资料大概意思就是在JMX中被管理的资源实例,通过MBean暴露的方法属性,外界可以获取被管理的资源状态并且操作MBean。
在本质上,MBean就是一个Java Object,外界可以通过反射来获取MBean的值或者调用其方法。MBean通过公共方法以及遵循特定的设计模式封装了属性和操作,以便暴露给管理应用程序。具体的话平时接触很少,大家如果有需要可以自行去了解,关于MBean相关知识我就不误人子弟了。
大家都知道真实的业务场景一般都部署在
Linux
一些服务器上,那么我们如果要排查服务器上应用程序的问题要怎么做呢?细心的小伙伴们都应该发现了,在我们最初连接JConsole时,提供了一个远程连接的选项,没错就是它。
但是这里我们连接也不是直接端上服务器IP就能连接上的,还需要我们配置一些JVM参数。
这里我们要使用JConsole主要需要配置这几个参数:-Dcom.sun.management.jmxremote.port=9999
、-Djava.rmi.server.hostname={ip}
、-Dcom.sun.management.jmxremote.ssl=false
、-Dcom.sun.management.jmxremote.authenticate=false
。
配置好之后我们启动应用程序,使用远程连接试一试。
可以看到这里连接是成功的,这个时候我们就可以通过远程连接去监控服务器上应用程序的一些内存、线程之类的具体情况,对应用程序进行问题排查和调优了。
除了
JConsole
,这里给大家介绍另外一款JDK自带的强大工具-VisualVM
。
VisualVM
应该是目前为止JDK内置工具中功能最强大的运行监控和故障处理工具。除了和JConsole
一样支持可视化外它也能够监控线程、内存情况、查看堆栈调用时间和消耗以及查看到内存中对象,还可以通过对象反向查看分配堆栈。
同样它也支持本地和远程连接应用程序,只要开启连接就可以对正在运行应用程序达到实时监控的效果。
VisualVM
使用也十分简单,我们再次找到JDK安装路径下的bin目录。
我们只要运行jvisualvm
就可以把VisualVM
启动起来了。这里我们可以简单看下主界面,首先和JConsole
一样也支持本地和远程方式去连接应用程序,还是比较方便的;另外同样无需我们自己再去手动执行jps
命令查找进程ID。
VisualVM
有一大亮点就是他可以安装插件,这就使这款简单的工具可以武装得非常强大了。我们在主页面点击上方页签工具
-插件
。
这里我们以Visual GC
为例,选中后点击安装即可。
安装成功后我们在已安装界面激活插件即可使用。
这里细心的小伙伴应该看到了又一个已下载
的页签。没错,除了官方自带的插件库,Visual VM
还支持我们自己去网上下载npm插件包进行激活,下载后从这里添加插件即可使用。
我们在本地开发的时候每次需要都去JDK目录下找还觉得繁琐?完全没问题,再让你偷个懒好了。这里以IDEA为例,我们在
Plugins
中搜索VisualVM
下载这个插件重启IDEA。
重新打开IDEA后发现在项目启动按钮旁边多了两个醒目的小按钮,是不是很熟悉?这里我们可以通过这两个按钮在Run
和Debug
模式下自动关联启动Visual VM
。
或者大家已经正常启动了应用程序,此时有需要通过Visual VM
分析排查问题,这里也提供了额外小按钮调起。
另外如果第一次使用这个插件的话是需要配置相关文件路径和目录的。这样使用起来是不是很方便呢?大家赶紧试试吧。
连上
Visual VM
主界面如上图,首页概述主要就是查看虚拟机进程相关信息、JVM参数、系统环境信息等,这是不是和我们之前所了解的jps
、jinfo
功能一样。
接着就是监视
模块,这里和JConsole
一样也提供了CPU、堆(包括Metaspace或方法区)、类、线程的大致使用情况,右上角还提供了一个堆Dump
功能,待会我们结合实例来看看怎样巧妙地使用这个功能。
线程
界面的话主要也就是可视化每个线程运行状态、消耗等相关信息,另外同样也提供线程Dump
功能。
抽样器
模块可以对CPU
、内存
进行抽样分析,还能够对CPU样例和堆内存分配生成快照。我们可以生成多份不同时间的快照文件,通过下图按钮进行比较多个文件的不同,从而可以分析出一些问题产生的原因。
这些就是Visual VM
给我们提供的强大功能模块,大家应该记得我们之前还安装过一个Visual GC
插件,这里我们来看看它能带给我们哪些信息。
Visual GC
将各代内存变化、GC频率、GC时耗清楚地展现在我们面前。其实Visual VM
的很多功能JConsole
基本也有,但是我们会发现Visual VM
更加直观并且数据分析更加全面,尤其是还提供添加插件功能,可以说是如虎添翼。
实践是检验整理的唯一标准,咱们直接来通过一些示例来看如何通过
Visual VM
分析程序问题。
说干咱就干,这里我们先来模拟一个OOM的情况,这里我们直接使用JConsole中
1.2.2 内存
的代码即可。
public class JconsoleMemoryAllocation {
public byte[] bytes = new byte[512 * 1024];
public static void main(String[] args) throws InterruptedException {
System.out.println("JconsoleMemoryAllocation main thread start...");
Thread.sleep(15000L);
allocation(6000);
}
private static void allocation(int size){
ArrayList<JconsoleMemoryAllocation> objects = new ArrayList<>();
for(int i = 0; i < size ; i++){
try {
Thread.sleep(100L);
objects.add(new JconsoleMemoryAllocation());
System.out.println("allocation current: " + i);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
已经成功出现OOM了,我们通过
Visual VM
这里我们通过
Visual GC
可以看到,当程序在持续运行过程中,老年代一直在进行GC操作,并且老年代中的堆内存使用量并未减少,大家通过我们对JVM的理解思考一下是什么原因导致的?
没错,大概率是存在无法被回收的对象,所以才导致我们最后OOM了。这里我们就需要进一步去分析是哪里哪个对象泄漏了。
首先我们通过抽样器中每个线程分配
,很清楚就可以看到可能是哪个线程发生了内存泄漏。这里我们进一步分析具体对象,主要有两种方式。
第一种:通过抽样器中内存快照,我们在生成两个不同时段的内存快照,使用我们开始提到的快照比照功能。
第二种就是通过监视
模块的堆Dump,再对两次结果进行比较。
这两种方法都可以让我们很快就确定具体泄漏的是哪种对象。
这里我们可以看出两次dump间隔时间内,JconsoleMemoryAllocation
和byte[]
实例在不停增加,说明这两个对象引用的方法可能存在内存泄漏。
我们进一步选中JconsoleMemoryAllocation
,右键选择在实例视图中显示
。
这里左侧实例数就是JconsoleMemoryAllocation
被创建的实例总数,右边上半部分是选中JconsoleMemoryAllocation
实例的结构,这里面可以看到它包含了一个很大的byte[]
数组。
而导致这些JconsoleMemoryAllocation
实例没有得到回收的原因正是因为右边下半部分,这里表明了这些实例被一个ArrayList
引用了,是不是通过Visual VM
一下就定位到了泄漏的根本原因?感觉肯定十分过瘾,那我们再来一个。
上面大家已经感受到了这款工具的强大,这里我们再来模拟一个线程死锁的情况,通过工具来分析究竟是哪里。
public class JVisualVMDeadLock {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock1){
try {
System.out.println(Thread.currentThread().getName() + "get lock1");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "get lock2");
}
}
}, "线程1").start();
new Thread(() -> {
synchronized (lock2){
try {
System.out.println(Thread.currentThread().getName() + "get lock2");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
synchronized (lock1){
System.out.println(Thread.currentThread().getName() + "get lock1");
}
}
}, "线程2").start();
}
}
这里点开
线程
模块,检测到死锁的字眼简直不要太亮眼。
我们再去点击旁边的线程Dump
,这里对哪个线程死锁,等待哪个资源也是十分清晰的。
另外我们还可以通过采样器
中CPU样例里的快照,很方便就能查看到出现问题的线程方法调用栈,从而快速排查问题。
之前在
JConsole
远程连接时,我们已经给服务器上的应用程序添加了jmx参数,这里我们直接使用。
右键Visual VM
中远程,添加远程主机。
添加好远程主机后,选择添加的远程主机右键添加JMX连接
,把服务器相关信息填上。
这样就成功连接上远程应用了,是不是十分简单呢?
这一章主要是接着上一章的JDK自带命令行工具的基础上,去介绍JDK自带工具里两款强大的可视化虚拟机工具。
我们通过这两章对JVM工具的介绍,如果能够巧妙灵活地去使用这些工具,无论是对于我们排查问题还是优化程序都会带来很大的便利。
我们在感叹时代进步、工具变得更强大的同时,也要不断地提升自我才能够更快更好地融入这个世界。后面我会一些常见的应用场景介绍一些JVM优化相关的东西,希望和大家一起进步,加油。