jps 虚拟机进程状况工具
jps [ options ] [ hostid ]
jstat:虚拟机统计信息监视工具
jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据,在没有GUI图形界面、只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的常用工具。
jstat命令格式为:
jstat [ option vmid [interval[s|ms] [count]] ]
参数interval和count代表查询间隔和次数,如果省略这2个参数,说明只查询一次。假设需要每250毫秒查询一次进程2764垃圾收集状况,一共查询20次,那命令应当是:
jstat -gc 2764 250 20
jstat -gcutil 2764
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 6.20 41.42 47.20 16 0.105 3 0.472 0.577
查询结果表明:这台服务器的新生代Eden区(E,表示Eden)使用了6.2%的空间,2个Survivor区(S0、S1,表示Survivor0、Survivor1)里面都是空的,老年代(O,表示Old)和永久代(P,表示Permanent)则分别使用了41.42%和47.20%的空间。程序运行以来共发生Minor GC(YGC,表示YoungGC)16次,总耗时0.105秒;发生Full GC(FGC,表示Full GC)3次,总耗时(FGCT,表示Full GCTime)为0.472秒;所有GC总耗时(GCT,表示GC Time)为0.577秒。
使用jstat工具在纯文本状态下监视虚拟机状态的变化,在用户体验上也许不如后文将会提到的JMC、VisualVM等可视化的监视工具直接以图表展现那样直观,但在实际生产环境中不一定可以使用图形界面,而且多数服务器管理员也都已经习惯了在文本控制台工作,直接在控制台中使用jstat命令依然是一种常用的监控方式。
jmap:Java内存映像工具
jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。如果不使用jmap命令,要想获取Java堆转储快照也还有一些比较“暴力”的手段譬如XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在内存溢出异常出现之后自动生成堆转储快照文件,通过-XX:+HeapDumpOnCtrlBreak参数则可以使用[Ctrl]+[Break]键让虚拟机生成堆转储快
照文件,又或者在Linux系统下通过Kill-3命令发送进程退出信号“恐吓”一下虚拟机,也能顺利拿到堆转储快照。
jmap的作用并不仅仅是为了获取堆转储快照,它还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。
jmap -dump:format=b,file=eclipse.bin 3500
Dumping heap to C:\Users\IcyFenix\eclipse.bin ...
Heap dump file created
jstack:Java堆栈跟踪工具
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。线程出现停顿时通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
可视化故障处理工具
JDK中除了附带大量的命令行工具外,还提供了几个功能集成度更高的可视化工具,用户可以使用这些可视化工具以更加便捷的方式进行进程故障诊断和调试工作。这类工具主要包括JConsole、JHSDB、VisualVM和JMC四个。其中,JConsole是最古老,早在JDK 5时期就已经存在的虚拟机监控工具,而JHSDB虽然名义上是JDK 9中才正式提供,但之前已经以sa-jdi.jar包里面的HSDB(可视化工具)和CLHSDB(命令行工具)的形式存在了很长一段时间。它们两个都是JDK的正式成员,随着JDK一同发布,无须独立下载,使用也是完全免费的。
JHSDB:基于服务性代理的调试工具
JDK中提供了JCMD和JHSDB两个集成式的多功能工具箱,它们不仅整合了上一节介绍到的所有基础工具所能提供的专项功能,而且由于有着“后发优势”,能够做得往往比之前的老工具们更好、更强大,表4-15所示是JCMD、JHSDB与原基础工具实现相同功能的简要对比。
本次,我们要借助JHSDB来分析一下代码清单4-6中的代码,并通过实验来回答一个简单问题:staticObj、instanceObj、localObj这三个变量本身(而不是它们所指向的对象)存放在哪里?
/**
* staticObj、instanceObj、localObj存放在哪里?
*/
public class JHSDB_TestCase {
static class Test {
static ObjectHolder staticObj = new ObjectHolder();
ObjectHolder instanceObj = new ObjectHolder();
void foo() {
ObjectHolder localObj = new ObjectHolder();
System.out.println("done"); // 这里设一个断点
}
}
private static class ObjectHolder {}
public static void main(String[] args) {
Test test = new JHSDB_TestCase.Test();
test.foo();
}
}
答案读者当然都知道:staticObj随着Test的类型信息存放在方法区,instanceObj随着Test的对象实例存放在Java堆,localObject则是存放在foo()方法栈帧的局部变量表中。这个答案是通过前两章学习的理论知识得出的,现在要做的是通过JHSDB来实践验证这一点。
请读者注意一下图中各个区域的内存地址范围,后面还要用到它们。打开Windows->Console窗口,使用scanoops命令在Java堆的新生代(从Eden起始地址到To Survivor结束地址)范围内查找
ObjectHolder的实例,结果如下所示:
果然找出了三个实例的地址,而且它们的地址都落到了Eden的范围之内,算是顺带验证了一般情况下新对象在Eden中创建的分配规则。再使用Tools->Inspector功能确认一下这三个地址中存放的对象,结果如图
Inspector为我们展示了对象头和指向对象元数据的指针,里面包括了Java类型的名字、继承关系、实现接口关系,字段信息、方法信息、运行时常量池的指针、内嵌的虚方法表(vtable)以及接口方法表(itable)等。由于我们的确没有在ObjectHolder上定义过任何字段,所以图中并没有看到任何实例字段数据
接下来要根据堆中对象实例地址找出引用它们的指针,JHSDB的Tools菜单中有ComputeReverse Ptrs来完成这个功能,果然找到了一个引用该对象的地方,是在一个java.lang.Class的实例里,并且给出了这个实例的地址,通过Inspector查看该对象实例,可以清楚看到这确实是一个java.lang.Class类型的对象实例,里面有一个名为staticObj的实例字段.
从《Java虚拟机规范》所定义的概念模型来看,所有Class相关的信息都应该存放在方法区之中,但方法区该如何实现,《Java虚拟机规范》并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。JDK 7及其以后版本的HotSpot虚拟机选择把静态变量与类型在Java语言一端的映射Class对象存放在一起,存储于Java堆之中,从我们的实验中也明确验证了这一点。
JConsole:Java监视与管理控制台
JConsole(Java Monitoring and Management Console)是一款基于JMX(Java Manage-mentExtensions)的可视化监视、管理工具。它的主要功能是通过JMX的MBean(Managed Bean)对系统进行信息收集和参数动态调整。JMX是一种开放性的技术,不仅可以用在虚拟机本身的管理上,还可以运行于虚拟机之上的软件中,典型的如中间件大多也基于JMX来实现管理与监控。虚拟机对JMX
MBean的访问也是完全开放的,可以使用代码调用API、支持JMX协议的管理控制台,或者其他符合JMX规范的软件进行访问。
“概述”页签里显示的是整个虚拟机主要运行数据的概览信息,包括“堆内存使用情况”“线程”“类”“CPU使用情况”四项信息的曲线图,这些曲线图是后面“内存”“线程”“类”页签的信息汇总,
“内存”页签的作用相当于可视化的jstat命令,用于监视被收集器管理的虚拟机内存(被收集器直接管理的Java堆和被间接管理的方法区)的变化趋势。我们通过运行代码清单4-7中的代码来体验一下它的监视功能。运行时设置的虚拟机参数为:
/**
* 内存占位符对象,一个OOMObject大约占64KB
*/
static class OOMObject {
public byte[] placeholder = new byte[64 * 1024];
}
public static void fillHeap(int num) throws InterruptedException {
List<OOMObject> list = new ArrayList<OOMObject>();
for (int i = 0; i < num; i++) {
// 稍作延时,令监视曲线的变化更加明显
Thread.sleep(50);
list.add(new OOMObject());
}
System.gc();
}
public static void main(String[] args) throws Exception {
fillHeap(1000);
}
这段代码的作用是以64KB/50ms的速度向Java堆中填充数据,一共填充1000次,使用JConsole的“内存”页签进行监视,观察曲线和柱状指示图的变化。
程序运行后,在“内存”页签中可以看到内存池Eden区的运行趋势呈现折线状,如图4-12所示。监视范围扩大至整个堆后,会发现曲线是一直平滑向上增长的。从柱状图可以看到,在1000次循环执行结束,运行了System.gc()后,虽然整个新生代Eden和Survivor区都基本被清空了,但是代表老年代的柱状图仍然保持峰值状态,说明被填充进堆中的数据在System.gc()方法执行之后仍然存活
提两个小问题供读者思考一下:
1)虚拟机启动参数只限制了Java堆为100MB,但没有明确使用-Xmn参数指定新生代大小,读者能否从监控图中估算出新生代的容量?
2)为何执行了System.gc()之后,图4-12中代表老年代的柱状图仍然显示峰值状态,代码需要如何调整才能让System.gc()回收掉填充到堆中的对象?
问题1答案:上图显示Eden空间为1,128,448 KB,因为没有设置-XX:SurvivorRadio参数,所以Eden与Survivor空间比例的默认值为8∶1,因此整个新生代空间大约为1,128,448 KB×125%。
问题2答案:执行System.gc()之后,空间未能回收是因为List<OOMObject>list对象仍然存活,fillHeap()方法仍然没有退出,因此list对象在System.gc()执行时仍然处于作用域之内。如果把System.gc()移动到fillHeap()方法外调用就可以回收掉全部内存。
线程监控
如果说JConsole的“内存”页签相当于可视化的jstat命令的话,那“线程”页签的功能就相当于可视化的jstack命令了,遇到线程停顿的时候可以使用这个页签的功能进行分析。前面讲解jstack命令时提到线程长时间停顿的主要原因有等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁等待等,代码清单4-8将分别演示这几种情况。
代码清单4-8 线程等待演示代码
package jvm.tools;
import java.io.BufferedReader;
import java.io.InputStreamReader;
/**
* @Description:
* @Created on 2020-04-06
*/
public class JconsoleTest {
/**
* 线程死循环演示
*/
public static void createBusyThread() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) // 第41行
;
}
}, "testBusyThread");
thread.start();
}
/**
* 线程锁等待演示
*/
public static void createLockThread(final Object lock) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "testLockThread");
thread.start();
}
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
br.readLine();
createBusyThread();
br.readLine();
Object obj = new Object();
createLockThread(obj);
}
}
程序运行后,首先在“线程”页签中选择main线程,如图4-13所示。堆栈追踪显示BufferedReader的readBytes()方法正在等待System.in的键盘输入,这时候线程为Runnable状态,Runnable状态的线程仍会被分配运行时间,但readBytes()方法检查到流没有更新就会立刻归还执行令牌给操作系统,这种等待只消耗很小的处理器资源。
程序运行后,首先在“线程”页签中选择main线程,如图4-13所示。堆栈追踪显示BufferedReader的readBytes()方法正在等待System.in的键盘输入,这时候线程为Runnable状态,Runnable状态的线程仍会被分配运行时间,但readBytes()方法检查到流没有更新就会立刻归还执行令牌给操作系统,这种等待只消耗很小的处理器资源。
接着监控testBusyThread线程,如图4-14所示。testBusyThread线程一直在执行空循环,从堆栈追踪中看到一直在MonitoringTest.java代码的41行停留,41行的代码为while(true)。这时候线程为Runnable状态,而且没有归还线程执行令牌的动作,所以会在空循环耗尽操作系统分配给它的执行时间,直到线程切换为止,这种等待会消耗大量的处理器资源。
testLockThread线程正处于正常的活锁等待中,只要lock对象的notify()或notifyAll()方法被调用,这个线程便能激活继续执行。代码清单4-9演示了一个无法再被激活的死锁等待。
package jvm.tools;
/**
* 线程死锁等待演示
*/
public class SynAddRunalbe implements Runnable {
int a, b;
public SynAddRunalbe(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
synchronized (Integer.valueOf(a)) {
synchronized (Integer.valueOf(b)) {
System.out.println(a + b);
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new SynAddRunalbe(1, 2)).start();
new Thread(new SynAddRunalbe(2, 1)).start();
}
}
}
这段代码开了200个线程去分别计算1+2以及2+1的值,理论上for循环都是可省略的,两个线程也可能会导致死锁,不过那样概率太小,需要尝试运行很多次才能看到死锁的效果。如果运气不是特别差的话,上面带for循环的版本最多运行两三次就会遇到线程死锁,程序无法结束。造成死锁的根本原因是Integer.valueOf()方法出于减少对象创建次数和节省内存的考虑,会对数值为-128~127之间的
Integer对象进行缓存[2],如果valueOf()方法传入的参数在这个范围之内,就直接返回缓存中的对象。也就是说代码中尽管调用了200次Integer.valueOf()方法,但一共只返回了两个不同的Integer对象。假如某个线程的两个synchronized块之间发生了一次线程切换,那就会出现线程A在等待被线程B持有的Integer.valueOf(1),线程B又在等待被线程A持有的Integer.valueOf(2),结果大家都跑不下去的情况.
出现线程死锁之后,点击JConsole线程面板的“检测到死锁”按钮,将出现一个新的“死锁”页签,
JIT生成代码反汇编
在《Java虚拟机规范》里详细定义了虚拟机指令集中每条指令的语义,尤其是执行过程前后对操作数栈、局部变量表的影响。这些细节描述与早期Java虚拟机(Sun Classic虚拟机)高度吻合,但随着技术的发展,高性能虚拟机真正的细节实现方式已经渐渐与《Java虚拟机规范》所描述的内容产生越来越大的偏差,《Java虚拟机规范》中的规定逐渐成为Java虚拟机实现的“概念模型”,即实现只保证与规范描述等效,而不一定是按照规范描述去执行。由于这个原因,我们在讨论程序的执行语义问题(虚拟机做了什么)时,在字节码层面上分析完全可行,但讨论程序的执行行为问题(虚拟机是怎样做的、性能如何)时,在字节码层面上分析就没有什么意义了,必须通过其他途径解决。
至于分析程序如何执行,使用软件调试工具(GDB、Windbg等)来进行断点调试是一种常见的方式,但是这样的调试方式在Java虚拟机中也遇到了很大麻烦,因为大量执行代码是通过即时编译器动态生成到代码缓存中的,并没有特别简单的手段来处理这种混合模式的调试,不得不通过一些曲线的间接方法来解决问题。在这样的背景下,本节的主角——HSDIS插件就正式登场了。
HSDIS是一个被官方推荐的HotSpot虚拟机即时编译代码的反汇编插件,它包含在HotSpot虚拟机的源码当中,在OpenJDK的网站[3]也可以找到单独的源码下载,但并没有提供编译后的程序。HSDIS插件的作用是让HotSpot的-XX:+PrintAssembly指令调用它来把即时编译器动态生成的本地代码还原为汇编代码输出,同时还会自动产生大量非常有价值的注释,这样我们就可以通过输出的
汇编代码来从最本质的角度分析问题。读者可以根据自己的操作系统和处理器型号,从网上直接搜索、下载编译好的插件,直接放到JDK_HOME/jre/bin/server目录(JDK 9以下)或JDK_HOME/lib/amd64/server(JDK 9或以上)中即可使用。如果读者确实没有找到所采用操作系统的对应编译成品,那就自己用源码编译一遍