7.5.4 分析工具
在大多数时候,Java程序本身并不需要对Java虚拟机有太多的了解。但是程序在运行过程中可能出现一些与虚拟机相关的错误,比如内存不足或线程死锁等问题。当出现这样的问题时,需要通过一些相关的工具来对虚拟机进行分析,找出问题的原因。对某些程序来说,可能需要对虚拟机的一些性能指标进行监视,以避免一些潜在的问题。Java虚拟机所提供的分析工具比较丰富,下面逐一进行介绍。
1.命令行和图形化工具
首先可以使用的工具是虚拟机在运行过程中输出的相关调试信息。在上一节中介绍了可以通过启动参数来控制虚拟机输出与垃圾回收和类加载相关的调试信息。
其次可以使用的是JDK中自带的命令行工具。第一组工具jmap和jhat用来对共享对象映射关系和堆内存进行分析。工具jmap可用于查看正在运行的Java程序、虚拟机的核心转储(core dump)文件,以及远程调试服务器上的共享对象的映射关系和堆内存的占用情况。使用jmap工具时,对于正在运行的Java程序,需要指定Java进程的标识符;对于虚拟机的核心转储文件,需要指定文件的路径;对于远程调试服务器,需要指定服务器的主机名或IP地址。在使用jmap时,可以指定一些选项来声明需要查看的内容。下面对jmap工具的介绍都以查看正在运行的Java程序为例来进行。
如果不指定任何选项,那么默认的行为是在控制台输出程序中共享对象的映射关系。对于每个虚拟机加载的共享对象,输出它在内存中的起始地址、映射空间的大小和共享对象所在的文件路径。在Windows操作系统中,被共享的对象通常是虚拟机所加载的操作系统中的DLL文件。如果添加“-heap”选项,那么会在控制台输出Java程序当前的堆内存占用情况的详细信息,其中包括所用的垃圾回收方式、堆内存的参数配置信息,以及当前堆内存中各个世代的不同区域的内存占用情况。如果添加“-finalizerinfo”选项,那么会在控制台输出Java程序中当前正在等待终止的对象的数量。添加“-histo”选项会在控制台输出堆内存占用情况的统计数据,对于每个Java类,输出其实例对象的个数和占用的内存。当使用“-histo:live”选项时,只会统计当前存活对象的信息。选项“-permstat”用来在控制台输出永久世代中包含的数据的统计信息。该信息主要由两部分组成,第一部分是被内部化的字符串的个数和占用的内存空间大小,第二部分是与类加载器相关的内容,包括每个类加载器已经加载的类的个数和占用的空间。选项“-dump”可以把当前程序的堆内存信息转储到文件中,再用jhat工具进行查看。
工具jhat用来对虚拟机堆内存的转储文件进行分析。其独特之处在于它会启动一个Web服务器。开发人员可以通过浏览器来查看转储文件中的各种内容。这种网页形式的查看方式既方便又直观。在使用jhat时需要指定转储文件的路径。在jhat工具运行起来后,可以通过默认的7000端口来访问Web界面。有多种方式可以得到堆内存的转储文件,如使用jmap、jconsole和hprof等工具,也可以在启动虚拟机时指定参数“-XX:+He apDumpOnOutOfMemoryError”。当虚拟机出现OutOfMemoryError错误时,会自动生成堆内存的转储文件。
比如,使用“jmap-dump:format=b, file=heap.bin 2456”命令可以把进程标识符为2456的虚拟机的堆内存转储到heap.bin文件中,再使用“jhat heap.bin”命令就可以启动jhat工具。通过浏览器就可以查看相关的信息,其中包括所加载的所有Java类、每个Java类的实例对象、堆中内存分布的统计数据和对象终止的情况等。对于每个对象实例,可以列出其中包含的数据成员。在对象的可达性方面,可以列出从一个对象实例可达的其他对象,还可以列出引用了一个对象实例的其他对象。前面介绍过,虚拟机在判断对象是否存活时从一些根对象开始遍历。使用jhat可以查看从根对象到当前对象的引用关系路径。通过这个功能可以发现程序中存在的内存泄露问题。如果发现某个应该被回收的对象仍然出现在内存中,可以检查它的引用关系路径,从而可以发现是由于哪个对象的引用关系仍然存在而造成当前对象无法被回收。
在jhat的Web界面中可以使用一种对象查询语言(Object Query Language, OQL)来查询堆内存的转储文件中的对象。OQL语言的语法类似SQL,按照“select……from……where”的结构来组织,其中表达式使用的是JavaScript语法。select子句中的内容是生成查询结果的JavaScript表达式,而from子句中的内容是待查询对象所在的Java类的全名,where子句中的内容则是用来进行过滤的条件表达式。例如,OQL语句“select s from java.lang.String s where s.count>=200”用来查询所有长度大于等于200的字符串对象。
除了堆内存的分析工具之外,还有其他一些工具可供使用,包括用来列出所有Java虚拟机进程标识符的jps命令行工具,查看虚拟机中线程堆栈信息的jstack工具,查看虚拟机中系统属性值和启动参数的jinfo工具,以及显示虚拟机运行性能相关指数的jstat工具。
以上介绍的这些工具都是命令行工具,通常把输出结果直接显示在控制台。JDK也提供了图形化界面的工具来监控虚拟机的状态。第一个可用的工具是jconsole。通过jconsole可以连接到正在运行的本地或远程Java程序上,获取程序所在虚拟机的内存、Java类和线程的相关信息。这些信息以图形化的方式显示出来,并且实时更新。
从前面的介绍中可以看到,JDK所包含的工具种类繁多,所提供的功能也不同。这么多的工具分开使用也不方便。从JDK 6更新7开始,新的图形化工具Java VisualVM被加入进来,通过jvisualvm命令可以启动这个工具。VisualVM的作用在于把各种不同工具的功能整合到了一起,用一个统一的图形化的方式展现出来。除此之外,VisualVM本身是一个可扩展的平台,可以利用社区贡献的插件来增强VisualVM本身对虚拟机的监控能力。VisualVM除了可以查看虚拟机的各种信息之外,还可以进行程序的性能取样和剖析,找出影响性能的瓶颈所在。
2.JMX
上面介绍的这些工具都是独立运行的,并没有提供相关的API接口供程序使用。从Java SE 5.0开始,Java平台提供了一套完整的API来对运行的Java程序及虚拟机本身进行监控和管理。这一套API由多个部分组成,其中最重要的组成部分是Java管理扩展(Java Management Extensions, JMX)API。JMX API是Java平台上进行资源监控和管理的标准API,可以用来对应用程序、设备、服务和Java虚拟机本身进行监控和管理。JMX API在很多情况下都可以发挥作用,包括在运行时动态获取和更新程序的配置信息、收集程序运行过程中的统计数据以及当程序内部状态发生变化或出现错误时发出相关通知等。JMX API的基础概念是MBean。一个MBean表示的是可以被管理的命名资源。每个MBean都提供一个管理接口允许第三方来使用。这个管理接口的内容包括可以获取和修改值的命名属性、可以调用的命名方法,以及可以发出的事件通知。
MBean本身只是一个接口,需要由被监控和管理的资源提供相关的实现。所有的MBean实现被注册到MBean服务器上。使用者通过名称在MBean服务器上查找所需MBean的实现。在得到了实现对象之后,可以通过MBean的管理接口来调用其中的方法。Java虚拟机本身提供了一个MBean服务器,可以在其上注册相关MBean。与Java虚拟机本身的监控和管理相关的MBean都注册在该MBean服务器上。相关MBean的接口都在java.lang.management包中定义,可以监控和管理的资源包括虚拟机中的缓冲区、类加载系统、代码编译、垃圾回收器、内存、底层操作系统、虚拟机运行时、线程和日志记录器等。通过java.lang.management.ManagementFactory类中的工厂方法可以得到所需的管理不同资源的MBean接口的实现,比如对线程的监控和管理是由接口java.lang.management.ThreadMXBean来表示的。如果需要获取当前虚拟机上的活动线程的数目,可以通过ManagementFactory类中的getThreadMXBean方法先得到ThreadMXBean接口的实现对象,再调用该对象的getThreadCount方法。对监控和管理其他资源的MBean的使用也是类似的。
利用对虚拟机平台的监控和管理的能力,可以根据虚拟机的运行情况来动态调整程序本身的运行逻辑。比如,某个程序中提供了后台运行的定时备份能力,程序中有一个线程在后台运行,并定期把程序中的重要数据备份到磁盘上,整个备份过程需要占用一定的内存空间,当虚拟机的内存空间不足时,备份过程应该暂停运行以免出现OutOfMemoryError错误而导致虚拟机退出,通过使用监控和管理虚拟机内存的java.lang.management.MemoryPoolMXBean接口就可以实现这样的功能。代码清单7-25给出了后台备份线程的实现示例。虚拟机通常把内存划分成多个区域,典型的是划分成多个世代。ManagementFactory类的getMemoryPoolMXBeans方法返回的是所有内存区域的列表。每个区域都有对应的进行监控和管理的MemoryPoolMXBean接口的实现对象。这里所关心的是年老世代所在的内存区域,因此使用的是名为“Tenured Bean”的MemoryPoolMXBean接口的实现对象。通过setUsageThreshold方法可以设置内存使用量的阈值。当超过这个阈值时,isUsageThresholdExceeded方法会返回true,说明剩余内存量已经不足。
代码清单7-25 考虑内存剩余量的备份任务的示例
public class BackupTaskRunnable implements Runnable{
private MemoryPoolMXBean poolBean;
public BackupTaskRunnable(){
init();
}
private void init(){
List<MemoryPoolMXBean>beans=ManagementFactory.getMemoryPoolMXBeans();
for(MemoryPoolMXBean bean:beans){
if("Tenured Gen".equals(bean.getName())){
poolBean=bean;
break;
}
}
poolBean.setUsageThreshold(1010241024);
}
public void run(){
while(true){
if(poolBean.isUsageThresholdExceeded()){
System.out.println("内存不足,暂停备份任务。");
}else{
System.out.println("执行备份任务。");
}
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
上面的代码使用的是轮询的方式来判断内存使用量是否超过阈值。还有一种做法是使用MBean的事件监听机制。某些MBean在内部状态发生变化或出现错误的时候,会产生相应的事件通知。MBean的使用者可以在事件通知上注册监听器。当事件发生的时候,所注册的监听器方法会被调用。这种方式类似于Web和桌面应用开发中的用户界面组件上的事件处理方式。
与虚拟机内存相关的java.lang.management.MemoryMXBean可以在内存区域的占用空间大小超过指定阈值时发出事件通知。对此事件感兴趣的程序只需要实现javax.management.NotificationListener接口并注册到MemoryMXBean对象上即可。代码清单7-26给出了相关的代码实现。
代码清单7-26 使用事件监听机制的内存监控示例
private static class MemoryListener implements NotificationListener{
public void handleNotification(Notification notification, Object handback){
String type=notification.getType();
if(type.equals(MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED)){System.out.println("内存占用量超过阈值。");
}
}
}
public void addListener(){
MemoryMXBean mbean=ManagementFactory.getMemoryMXBean();
NotificationEmitter emitter=(NotificationEmitter)mbean;
MemoryListener listener=new MemoryListener();
emitter.addNotificationListener(listener, null, null);
}