如果小伙伴们从第一章看到现在,那么我相信大家对JVM已经有了一定认识了,但是我们也需要学会武装自己才能够彻底征服JVM,虚拟机工具自然而然就是最好的武器。
当我们给一个程序系统定位JVM相关问题时,我们对知识的理解就像游戏里我们对角色的的理解;我们处理问题的经验就像我们角色的能力值;数据就像是地图上所能利用的资源;而工具就是我们通关所运用的手段。
而这里面的数据就包括:运行日志
、异常堆栈
、GC日志
、线程快照
、堆转储快照
等。如果能够合理并且熟练地使用这些虚拟机工具,可以对数据进行快速分析并且提高定位问题解决问题的效率。这里我们就对一些常用的虚拟机工具进行介绍,让自己变得更加强大。
大家对Linux应该都不会默认,我们经常会使用一个命令就是
ps -ef|grep
。
grep
命令主要就是用于查找,|
是管道命令可以使ps
和grep
同时执行。ps
也是Linux中最常用而且非常强大的查看进程命令,而grep
则是一种十分强大的文本搜索命令,还可以使用正则表达式将匹配的文本进行输出。假如我们需要查找正在运行的java程序,则可以使用ps -ef|grep java
。
在介绍
JPS
前我们先看一样东西,就是我们熟悉的JDK
。我们找到JDK
安装路径找到bin
目录,可以看到这里面有很多应用程序,这其中就包括jps
、jmap
等。
再找到lib
目录下tools.jar
,打开之后可以看到这里面其实就包含了我们所看到jps
等命令的源码,所以JDK
本身其实就提供了许多虚拟机相关的工具来方便我们发现、分析以及解决虚拟机的问题。
jps
就是其中比较典型的JVM工具,我们会发现名称和ps
命令很相像,而其实功能也与ps
命令相似。我们可以通过jps
命令显示出虚拟机执行主类(Main Class)名称以及其进程对应的本地虚拟机标识(Local Virtual Machine Identifier-LVMID)
,虽然功能比较单一但却是使用频率最高的工具。
jps
命令的使用很简单,这里我们随手启动一个之前的项目,分别介绍一下几个参数的作用。
输出程序主类全名,若进程执行的是Jar包则输出Jar包路径
输出虚拟机进程启动时传递给主类main()函数的参数。
输出虚拟机进程JVM参数,这里我们可以看到我们之前示例自己所设置的JVM参数。这个命令使用起来也很简单,相信大家也很熟悉就不过多介绍了。
jstat
命令也是我们JDK包中自带的小工具,主要用于监视虚拟机各种运行状态信息。可以显示Java应用程序运行时的类装载、内存使用、垃圾收集、JIT编译等运行状况。若不适用GUI图形界面工具进的话,那么它就是定位虚拟机性能问题的首选工具。
这里我们输入jstat -help
查看一下有哪些参数可以供我们使用,另外提一句当大家不知道其他命令如何使用时,一般直接输入命令例如jstat
或者jstat -help
都会提示相关用法介绍。
- option:参数选项,下面会介绍有哪些参数可以使用
- -t:显示Timestamp列,用于显示系统运行时间
- -h:后跟数字,隔几行显示标题
- vmid:VM进程ID
- interval:监控执行间隔(单位ms)
- count:监控执行次数(默认循环执行)
上面我们对基本的参数做了一个简单介绍,这里我们再通过
jstat -options
来看看options参数能够怎么选择,接下来我们通过实际情况给大家分别介绍下每个参数选项的用法。
显示ClassLoad的装载、卸载数量以及所占空间和耗费时间,这里
8468
就是我们通过jps
获取的进程pid,250表示250ms执行一次,10表示总共执行次数。
显示应用程序GC相关堆信息,主要包括Eden区、Survivor区、老年代、永久代等容量使用情况以及GC时间消耗等信息。
- S0C:年轻代第一个Survivor幸存区的容量
- S1C:年轻代第二个Survivor幸存区的容量
- S0U:年轻代第一个Survivor幸存区的已使用空间大小
- S1U:年轻代第二个Survivor幸存区的已使用空间大小
- EC:年轻代中Eden区的容量
- EU:年轻代中Eden区的已使用空间大小
- OC:老年代容量
- OU:老年代已使用空间大小
- MC:方法区容量
- MU:方法区已使用空间大小
- CCSC:压缩类空间容量
- CCSU:压缩类空间已使用大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
显示各分代容量及使用情况。
- NGCMN:年轻代初始化(最小)容量
- NGCMX:年轻代最大容量
- NGC:年轻代当前容量
- S0C:年轻代第一个Survivor幸存区的容量
- S1C:年轻代第二个Survivor幸存区的容量
- EC:年轻代中Eden区的容量
- OGCMN:老年代初始化(最小)容量
- OGCMX:老年代最大容量
- OGC:老年代当前新生成容量
- OC:老年代容量
- MCMN:元空间初始化(最小)容量
- MCMX:元空间最大容量
- MC:元空间当前新生成容量
- CCSMN:最小压缩类空间容量
- CCSMX:最大压缩类空间容量
- CCSC:当前压缩类空间大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
这里我们也可以通过
-gcnewcapacity
、-gcoldcapacity
、-gcmetacapacity
分别查看年轻代、老年代以及元空间容量以及使用情况。
主要用于显示GC统计信息。
- S0:年轻代第一个Survivor幸存区已使用容量百分比
- S1:年轻代第二个Survivor幸存区已使用容量百分比
- E:年轻代中Eden已使用容量百分比
- O:老年代已使用容量百分比
- M/P:元空间(JDK1.8以前Perm永久代)已使用容量百分比
- CCS:压缩类已使用容量百分比
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
内容和
-gcutil
大致一样,显示GC相关信息,会额外显示最后一次或正在进行的GC原因。
显示JIT编译相关信息。
- Compiled:编译任务执行数量
- Failed:编译任务执行失败数量
- Invalid:编译任务执行失效数量
- Time:编译任务消耗时间
- FailedType:最后一个编译失败任务类型
- FailedMethod:最后一个编译失败任务信息
显示已经被JIT编译的方法信息。
- Compiled:编译任务数量
- Size:方法生成字节码大小
- Type:编译类型
- Method:方法标识(类名、方法名)
显示加载Class相关信息。
- Loaded:已装载类数量
- Bytes:装载类所占字节数
- Unloaded:已卸载类数量
- Bytes:卸载类所占字节数
- Time:装载以及卸载类所耗时间
jinfo
命令的作用主要是实时查看虚拟机各项参数。我们知道使用jps -v
命令可以查看虚拟机启动时显示指定的参数列表,但是我们想要知道除了显示指定的其他参数要如何做呢?jinfo
就给我们提供了这个功能。
这里我们通过jinfo pid
可以看到经过一个短时间的等待后,会将很多很多信息输出给我们,这里面就包括各种应用环境配置、JVM参数配置等,小伙伴们可以自己动手去执行看看。如果大家想要查看程序详细的配置,那么jinfo
命令是你的不二选择。
jmap
命令主要可以用于生成堆转储快照。和jinfo
命令一样,如果大家在windows平台执行某些命令失效是正常的,有些功能在Windows下是不支持的。
除了可以获取dump文件,我们还能够通过jmap
查询到堆中各代空间使用率等情况,并且可以查看到每种类的实例、空间占用等信息。
另外我们除了jmap
生成堆转储快照之外,还可以通过JVM启动参数中设置-XX:+HeapDumpOnOutOfMemoryError
让程序在OOM后自动生成dump文件。然后通过分析dump文件去解决相关问题。
显示堆中详细信息,包括参数配置、使用垃圾收集器、各代空间使用等情况。
-histo
命令是我们上章使用过的一个命令,大家可能会有一点印象。当时我们通过jmap -histo:live 2772>jmap_histo.log
命令将堆中存活对象统计信息输出到了日志文件中用于分析开启逃逸分析的效果。这里我们也可以通过jmap -histo pid
直接去查看堆中对象信息,另外如果因为信息太多的我们可以通过jmap -histo pid|more
自己去分页查看。
这里面主要包括类、实例数量、所占字节量。另外我们可以看到其中有几个比较特殊的标识:【I、【B、【C,这几个其实就是我们所熟悉的int、byte、char类型数据。
dump
命令主要作用就是用来生成堆转储快照,可供我们对程序运行情况进行分析。主要格式就是jmap -dump:format=b,file=xxx pid
我们在之前介绍了很多例如GC日志、JVM启动参数、堆转储快照等,那么这些日志怎么分析、参数怎么设置呢?
除了我们通过自己的经验之外,这里我给大家安利一个网站【PerfMa】。
这个网站提供了很多可视化分析的界面,并且还能够通过你不同机器的硬件配置情况制定不同的JVM启动参数。这里我们就用我们上面dump
下的堆转储快照为例。
这里导入dump文件也十分方便,直接拖拽即可。
大家可以看到分析生成后的可视化界面真的十分强大,堆内存使用情况、GC ROOT个数、线程个数以及每个类的实例个数、所占容量大小甚至类加载器、每个对象信息都能够看到,还支持一些常用的条件搜索可以定位到我们需要精准查找的内容。
大家可以自己使用感受一下他的强大。如果我们自身已经对JVM有了一定的了解,那么这些强大的可视化分析工具就不是阻碍我们成长的羁绊而是助力器。
当然如果我们没有上面介绍的可视化分析工具的话,我们要如何去分析dump文件呢?答案就是
jhat
命令,这个命令经常和jmap
搭配使用,主要作用和上面类似就是用于分析堆转储快照。
jhat
可以将dump文件进行分析后通过浏览器去进行查看,但是需要注意的是jhat
分析工作是一个耗时且对硬件资源有消耗的过程,整个分析功能也比较简陋,不过我们这里也来看看他到底是会怎样进行分析。
我们使用上面的dump文件。
如果显示上面这样就是启动成功了,我们可以看到他提示默认端口为7000,这里我们尝试进行访问。
我们可以看到jhat
会通过分析dump文件帮我们生成一个这样的界面,这里面也包括一些堆使用情况,这些数据大家是不是十分熟悉。另外jhat
还提供了一个OQL查询功能。
这里我们可以通过OQL语句去按照条件查询对象情况,如果大家想要尝试的话可以看看这里也提供了一个OQL HELP
,不过如果我们已经有了更强大的工具的话可能就很少会去使用这些命令了。
我们上面讲的那么多命令,会发现很多都是和堆内存信息有关的信息,那么在我们项目中除了堆中内存管理还有什么是我们经常会碰到并且总是难以下手的呢?没错,就是线程。
jstack
命令就是用于生成JVM当前时刻的线程快照(threaddump)。线程快照其实就是当前JVM中每条线程正在执行的方法堆栈的一个集合。
当线程死锁、死循环、请求外部资源长时间等待时都可能造成线程长时间停顿,我们生成线程快照的目的就是为了定位并解决这些问题。当线程出现停顿时我们可以通过jstack
来查看各线程调用堆栈链,从而分析线程当前状态以及造成问题的原因。
同样我们启动开始的程序,通过
jps
命令获取进程PID。这里我们通过jstack -l 4728
命令就会输出当前时刻线程快照了,我们可以看到这里面包括每个线程的状态、标识信息、堆栈调用链等,还是很详细的,我们可以通过这些信息去分析线程停顿造成的原因从而找到解决方案。下面我们来一个示例看看当我们线程死锁了会是怎样的。(线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行)
public class DeadLock {
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();
}
}
大家应该都知道线程是什么,就是两个或多个线程互相持有对方所需要的资源,导致这些线程都处于一个等待状态,从而造成线程死锁。
我们这里用这个简单的例子来模拟一下线程死锁,我们启动之后会发现应用处于等待状态,这个时候我们通过开始的命令jstack -l pid
。
这里可以看到他帮我们分析出了发现了一处死锁,并且把相关的线程信息和方法调用堆栈位置都给我们标记出来了,同时还显示出了死锁造成的原因是由于等待哪一个锁造成的。通过线程快照我们可以很快地定位线程停顿的原因,大家可以自己动手去试一试感受一下。
这里提到线程那就大家一起简单复习一下线程有哪几种状态。
- NEW:线程创建状态。
- RUNNABLE:线程执行状态。
- BLOCKED :线程阻塞状态。一般情况下是线程正在等待获取一个锁,像我们上面死锁的示例就是,在未获取锁之前都会是该状态。如果长时间处于该状态则需要考虑是否死锁。
- WAITING :线程等待状态。当我们平时执行了
Object.wait()
或Thread.join()
都会使线程变为该状态,直到另一个线程执行Object.notify()
等相关代码时才会被唤醒,这种状态有意而为的话是可以没有时间限制的。- TIMED_WAITING :线程有限等待状态。上面的
WAITING
的话可以理解为是没有时间限制的等待,只有符合某种条件唤醒才会继续执行;而该状态等待一定时间后会主动去唤醒线程获取资源。- TERMINATED:线程结束状态。
这里我们主要就是稍微理解一下
WAITING
和BLOCKED
,前者是主动显示申请阻塞,后者属于被动阻塞。另外一个就是WAITING
和TIMED_WAITING
,前者可以无限期等待而后者有一个时间限制。
这一章我们主要介绍了JDK自带工具包中一些关于JVM应用程序相关的命令,可以帮助我们去查看应用程序的一些堆存储状态、应用程序信息、线程状态等,让我们可以在有问题或者需要时对其运行状况进行了解。
随着JAVA的发展,也有越来越多更成熟更好用的可视化工具可以帮助我们对应用程序进行分析,下一章我们就来对这些可视化工具进行一个了解和使用。