学习JVM的第四章,使我了解了JDK发布的6个命令行工具及两个可视化的故障处理工具,灵活使用这些工具可以给问题处理带来很大的便利。除了JDK自带的工具之外,常用的故障处理工具还有很多,如果使用的而是非Sun系列的JDK,非HotSpot的虚拟机,就需要使用对应的工具进行分析。
前面两章对于虚拟机内存分配与回收技术各方面的介绍,相信可以建立一套比较完整的理论基础。这一章是从实践的角度去了解虚拟机内存管理的世界。给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。这里说的数据包括:运行日志、异常堆栈、GC日志、线程快照(threaddump/javacore文件)、堆转储快照(headdump/hprof文件)等。经常使用适当的虚拟机监控和分析工具可以加快我们分析数据、定位解决问题的速度,但在学习工具前,也应当意识到工具永远都是知识技能的一层包装。
上图4-1的.exe工具(这些工具都很小,是因为这些命令行工具大多数是JDK/lib/tools.jar类库的一层薄包装而已),它们的主要的功能代码是在tools类库中实现的。
下表是Sun JDK监控和故障处理工具
名称 |
主要作用 |
jps |
JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程 |
jstat |
JVM Statistics Monitoring Tool,用于收集HotSpot虚拟机各方面的运行数据 |
jinfo |
Configuration Info for Java,显示虚拟机配置信息 |
jmap |
Memory Map for Java,生成虚拟机的内存转储快照(Heapdump文件) |
jhat |
JVM Heap Dump Browser,用于分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果 |
jstack |
Stack Trace for Java,显示虚拟机的线程快照 |
还有我们注意到Sun开发团队选择采用Java代码来实现这些监控工具是有特别用意的:当应用程序部署到生产环境后,无论是直接接触物理服务器还是远程Telnet到服务器上都可能会受到限制。借助tools.jar类库里面的接口,我们可以直接在应用程序中实现功能强大的监控分析功能。
功能:可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)。(虽然功能比较单一,但它是使用频率最高的JDK命令行工具,因为其他的JDK命令行工具大多需要输入它查询到LVMID来确定要监控的是哪一个虚拟机进程。对于本地虚拟机进程来说,LVMID与操作系统的进程ID(Process Identifier,PID)是一致的,使用Windows的任务管理器或者unix的ps命令也可以查询到虚拟机进程的LVMID,但如果同时启动了多个虚拟机进程,无法根据进程名称定位时,那就只能依赖jps命令来显示主类的功能才能区分了)。
jps命令格式:
jps [ option ] [ hostid ]
jps可以通过RMI协议查询开启了RMI服务的远程虚拟机进程状态,hostid为RMI注册表中注册的主机名。jps的其他常用选项见下表:
选项 |
作用(jps工具主要选项) |
-q |
只输出LVMID,省略主类的名称 |
-m |
输出虚拟机进程启动时传递给主类main()函数的参数 |
-l |
输出主类的全名,如果进程执行的是Jar包,输出Jar路径 |
-v |
输出虚拟机进程启动时JVM参数 |
jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首先工具。
jstat命令格式为:
jstat [ option vmid [ interval [ s | ms ] [ count ] ] ]
对于命令格式中的VMID与LVMID需要特别说明一下:如果是本地虚拟机进程,VMID与LVMID是一致的,如果是远程虚拟机进程,那VMID的格式应当是:
[ protocol :] [ // ] lvmid [ @hostname [ :port] /servername]
参数interval和count代表查询间隔和次数,如果省略这两个参数,说明只查询一次。假设需要每250毫秒查询一次进程2764垃圾收集状况,一共查询20次,那命令应当是:
jstat -gc 2764 250 20
选项option代表着用户希望查询的虚拟机信息,主要分为3类:类装载、垃圾收集、运行期编译状况,具体选项及作用请参考表中的描述:
选项 |
作用(jstat工具主要选项) |
-class |
监视类装载、卸载数量、总空间以及类装载所耗费的时间 |
-gc |
监视Java堆状况,包括Eden区、两个survivor区、老年代、永久代等的容量、已用空间、GC时间合计等信息 |
-gccapacity |
监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大。最小空间 |
-gcutil |
监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比 |
-gccause |
与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因 |
-gcnew |
监视新生代GC状况 |
-gcnewcapacity |
监视内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间 |
-gcold |
监视老年代GC状况 |
-gcoldcapacity |
监视内容与-gcold基本相同,输出主要关注使用到的最大、最小空间 |
-gcpermcapacity |
输出永久代使用到的最大、最小空间 |
-compiler |
输出JIT编译器编译过的方法、耗时等信息 |
-printcompilation |
输出已经被JIT编译的方法 |
jinfo(Configuration Info for Java)的作用是实时地查看和调整虚拟机各项参数。
jinfo命令格式:
jinfo [ option ] pid
选项 |
作用(jinfo工具主要选项) |
-v |
可以查看虚拟机启动时显示指定的参数列表 |
-flag |
查看未被显示指定的参数的系统默认值 |
Java -XX:PrintFlagsFinal |
查看参数默认值也是一个很好的选择 |
-sysprops |
把虚拟机进程的System.getProperties()的内容打印出来 |
jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为headdump或dump文件)。如果不使用jmap命令,要想获取Java堆转储快照,还有一些比较“暴力”的手段:譬如在第2章中用过的-XX:+HeapOnOutOfMemoryError参数,可以让虚拟机在OOM异常出现之后自动生成dump文件,通过-XX:+HeapDumpOnCtrlBreak参数则可以使用[ Ctrl ] + [ Break ]键让虚拟机生成dump文件,又或者在Linux系统下通过Kill -3命令发送进程退出信号“吓唬”一下虚拟机,也能拿到dump文件。
jmap的作用并不仅仅是为了获取dump文件,它可以查询finalize执行队列、Java堆和永久代的信息信息,如空间使用率、当前用的是哪种收集器等。
和jinfo命令一样,jmap有不少功能在Windows平台下都是受限的,除了生成dump文件的-dump选项和用于查看每个类的实例、空间占用统计的-histo选项在所有操作系统都提供之外,其余选项都只能在Linux/Solaris下使用。
jmap命令格式:
jmap [ option ] vmid
option选项的合法值与具体含义见下表:
选项 |
作用 |
-dump |
生成Java堆转储快照。格式为:-dump:[ live, ] format=b, file= |
-finalizerinfo |
显示在F-Queue中等待Finalizer线程执行finalize方法的对象。只在Linux/Solaris平台下有效 |
-heap |
显示Java堆详细信息,如使用哪种回收器、参数配置、分代状况等。只在Linux/Solaris平台下有效 |
-histo |
显示堆中对象统计信息,包括类、实例数量、合计容量 |
-permstat |
以ClassLoader为统计口径显示永久代内存状态。只在Linux/Solaris平台下有效 |
-F |
当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照。只在Linux/Solaris平台下有效 |
Sun JDK提供jhat(JVM heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看。(一般不会直接使用jhat命令来分析dump文件,原因有两点:1,一般不会在部署应用程序的服务器上直接分析dump文件,即使这样做,一般也尽量把dump文件复制到其他机器上去分析,因为分析是一个耗时且消耗硬件资源的一个过程,既然都要在其他机器上运行了,就没有必要受到命令行工具的限制了;2,jhat的分析功能相对来说比较丑陋,有其工具能实现比jhat更强大更专业的分析功能)。
分析结果默认是以包为单位进行分组显示,分析内存泄漏问题主要会使用到其中的“Heap Histogram”(与jmap -histo功能一样)与OQL页签的功能,前者可以找到内存中总容量最大的对象,后者是标准的对象查询语言,使用类似SQL的语法对内存中的对象进行查询统计。
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待都是导致线程长时间停顿的常见原因。线程出现停顿的时候使用jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程在后台做些什么事情,或者等待着什么资源。
jstack命令格式:
jstack [ option ] vmid
选项 |
作用(jstack工具主要选项) |
-F |
当正常输出的请求不被响应时,强制输出线程堆栈 |
-l |
除堆栈外,显示关于锁的附加信息 |
-m |
如果调用到本地方法的话,可以显示C/C++的堆栈 |
在JDK1.5中,java.lang.Thread类新增了一个getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码就完成了jstack的大部分功能,在实际项目中不妨调用这个方法做个管理员界面,可以随时使用浏览器来查看线程堆栈,如下面代码清单所示:
<%@ page import = "java.util.Map" %>
服务器线程信息
<%
for(Map.Entry stackTrace : Thread.getAllStackTraces().entrySet()){
Thread thread = (Thread)stackTrace.getKey();
StackTraceElement[] stack = (StackTraceElement[]) stackTrace.getValue();
if(thread.equals(Thread.currentThread())){
continue;
}
out.print("\n线程:" + thread.getName() + "\n");
for(StackTraceElement element : stack){
out.print("\t" + element + "\n");
}
}
%>
HSDIS是一个Sun官方推荐的HotSpot虚拟机JIT编译代码的反汇编插件,它包含在HotSpot虚拟机源码之中,但没有提供编译后的程序。在Project Kenai的网站也可以下载到单独的源码。它的作用是让HotSpot的-XX:+PrintAssembly指令调用它来把动态生成的本地代码还原为汇编代码输出,同时还生成了大量非常有价值的注释,这样我们就可以通过输出的代码来分析问题。读者可以根据自己的操作系统和CPU类型从Project Kenai的网站上下载编译好的插件,直接放到JDK_HOME/jre/bin/client和JDK_HOME/jre/bin/server目录中即可。如果没有找到所需操作系统(譬如windows的就没有)的成品,那就得自己使用源码编译一下。(值得注意的是:如果读者使用的是Debug或者FastDebug版的HotSpot,那就可以直接通过-XX:PrintAssembly指令使用插件;如果使用的是Product版的HotSpot,那还需要额外加入一个-XX:UnlockDiagnosticVMOptions参数)。JVM书的作者曾以下面简单地代码为例演示了这个插件的使用。
public class Bar{
int a = 1;
static int b = 2;
public int sum(int c){
return a + b + c;
}
public static void main(String[] args) {
new Bar().sum(3);
}
}
对上图代码进行解释:
1:mov %eax,-0x8000(%esp):检查栈溢;
2:push %ebp:保存上一帧栈基址;
3:sub $0x18,%esp:给新帧分配空间;
4:mov 0x8(%ecx) ,%eax:取实例变量a,这里0x8(%ecx)的意思是ecx+0x8的意思,前面“[Constants]”节中提示了“this.ecx = ‘test/Bar’”,即ecx存放的是this对象的地址。偏移0x8是越过this对象的对象头,之后就是实例变量a的内存位置。这次是访问“Java堆”中的数据;
5:mov $0x3d2fad8,%esi:取test.Bar方法区的指针;
6:mov 0x68(%esi),%esi:取类变量b,这次是访问“方法区”中的数据;
7:add %esi,%eax和add %edx,%eax:做两次加法,求a+b+c的值,前面的代码把a放在eax中,把b放在esi中,而c在[Constants]中提示了,“parm0:edx = int”,说明c在edx中;
8:add $0x18,%esp:撤销帧栈;
9:pop %ebp:恢复上一帧栈;
10:test %eax,0x2b0100:轮询方法返回处的SafePoint;
11:ret:方法返回
JDK中除了提供大量的命令行工具外,还有两个功能强大的可视化工具:JConsole和VisualVM,这两个工具是JDK的正式成员,没有被贴上“unsupported and experimental”的标签。
JConsole(Java Monitoring and Management Console)是一种基于JMX的可视化监视、管理工具。它管理部分的功能是针对JMX MBean进行管理,由于MBean可以使用代码、中间服务器的管理控制台或者所有符合JMX规范的软件进行访问。
1:启动JConsole:
通过JDK/bin目录下的“jconsole.exe”启动JConsole后,将自动搜索出本机运行的所有虚拟机进程,不需要用户再使用jps来查询了。如下图所示:双击选择其中一个进程即可进行监控,也可以使用下面的“远程进程”功能来连接远程服务器,对远程虚拟机进行监控。
选择monitoring这个双击它进入JConsole主页面
2:内存监控
“内存”页签相当于可视化的jstat命令,用于监视受收集管理的虚拟机内存(Java堆和永久代)的变化趋势。
import java.util.ArrayList;
import java.util.List;
public class JConsoleMonitor {
static class OOMObject{
public byte[] placeholder = new byte[64 * 1024];
}
public static void fillHeap(int num)throws InterruptedException{
List list = new ArrayList();
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);
}
}
执行上面程序代码可以在“内存”页签中可以看到内存池Eden区的运行趋势呈折线,如下图所示:
但是监视范围扩大至整个堆后,会发现曲线是一条向上增长的平滑曲线。并且从柱状图可以看出,在1000次循环执行结束,运行了System.gc()后;虽然整个新生代Eden和Survivor区基本都被清空了,但是代表老年代的柱状图仍然保持巅峰值状态,说明被填充进堆中的数据在System.gc()方法执行之后仍然存活。但是有两个问题:
1:虚拟机启动参数只限制了Java堆为100MB,没有指定-Xmn参数,能否从监控图中估计出新生代有多大?
上图显示Eden空间为27328KB,因为没有设置-XX:SurvivorRadio参数,所以Eden与Survivor空间比例默认值为8:1,整个新生代空间大约为27328KB*125%=34160KB;
2:为何执行了System.gc()之后,上面的图老年代依然显示巅峰值状态,代码需要做何调整才能让System.gc()回收掉填充到堆中的对象?
执行完System.gc()之后,空间未能回收是因为List
3:线程监控
如果上面的“内存”页签相当于可视化的jstat命令的话,“线程”页签的功能相当于可视化的jstack命令,遇到线程停顿时可以使用这个页签进行监控分析。前面讲解jstack命令的时候提到过线程长时间停顿的主要原因主要有:等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁等待(活锁和死锁)。
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class ThreadWait {
/**
* 线程死循环演示
*/
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线程,如下图所示:堆栈追踪显示BufferedReader在readBytes方法中等待System.in的键盘输入,这是线程为Runnable状态,Runnable状态的线程会被分配运行时间,但readBytes方法检查到流没有更新时会立刻归还执行令牌,这种等待只消耗很小的CPU资源。
接着监控testBusyThread线程,如下图所示,testBusyThread线程一直在执行空循环,从堆栈追踪中看到一直在MonitoringTest.java代码的41行停留,41行为:while(true)。这时候线程为Runnable状态,而且没有归还线程执行令牌的动作,会在空循环上用尽全部执行时间直到线程切换,这种等待会消耗较多的CPU资源。
下图显示testLockThread线程在等待着lock对象的notify或notifyAll方法的出现,线程这时处于WAITING状态,在被唤醒前不会被分配执行时间。
testLockThread线程正在处于正常的活锁等待,只要lock对象的notify()或notifyAll()方法被调用,这个线程便能激活以继续执行。下面代码演示了一个无法再被激活的死锁等待。
public class DeadlockWait {
/**
* 线程死锁等待演示
*/
static class SynAddRunnable implements Runnable{
int a, b;
public SynAddRunnable(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++){//开启200个线程分别取计算1+2以及2+1的值,其实for循环可以省略,两个线程也可能会出现死锁,不过概率太小
new Thread(new SynAddRunnable(1,2)).start();
new Thread(new SynAddRunnable(2,1)).start();
}
}
}
出现线程死锁之后,点击JConsole线程面板的“检测到死锁”按钮,将出现一个新的“死锁”页签,如下图所示,很清楚的看大Thread-43在等待一个被线程Thread-12持有的Integer对象,而点击线程Thread-12则显示它也在等待一个Integer对象,被线程Thread-43持有,这样两个线程就相互卡主,都不存在等到锁释放的希望了。
VisualVM(All-in-One Java Troubleshooting Tool)是到目前为止随着JDK发布的功能最强大的运行监视和故障处理程序。它的性能分析也比一些收费软件不相上下,它还有一个优点:不需要被监视的程序基于特殊Agent运行。
1:VisualVM兼容范围与插件安装
通过插件扩展支持,VisualVM可以做到:
2.生成、浏览堆转储快照
3.分析程序性能
4.BTrace动态日志跟踪