今天我们来继续了解Java虚拟机的监控及诊断工具。

# eclipse MAT

在上一篇中,我介绍了jmap工具,它支持导出Java虚拟机堆的二进制快照。eclipse的MAT工具便是其中一个能够解析这类二进制快照的工具。

MAT本身也能够获取堆的二进制快照。该功能将借助jps列出当前正在运行的Java进程,以供选择并获取快照。由于jps会将自己列入其中,因此你会在列表中发现一个已经结束运行的jps进程。

MAT获取二进制快照的方式有三种,一是使用Attach API,二是新建一个Java虚拟机来运行Attach API,三是使用jmap工具。

这三种本质上都是在使用Attach API。不过,在目标进程启用了DisableAttachMechanism参数时,前两者将不在选取列表中显示,后者将在运行时报错。

当加载完堆快照之后,MAT的主界面将展示一张饼状图,其中列举占据的Retained heap最多的几个对象。

这里讲一下MAT计算对象占据内存的两种方式。第一种是Shallow heap,指的是对象自身所占据的内存。第二种是Retained heap,指的是当对象不再被引用时,垃圾回收器所能回收的总内存,包括对象自身所占据的内存,以及仅能够通过该对象引用到的其他对象所占据的内存。上面的饼状图便是基于Retained heap的。

MAT包括了两个比较重要的视图,分别是直方图(histogram)和支配树(dominator tree)。

MAT的直方图和jmap-histo子命令一样,都能够展示各个类的实例数目以及这些实例的Shallow heap总和。但是,MAT的直方图还能够计算Retained heap,并支持基于实例数目或Retained heap的排序方式(默认为Shallow heap)。此外,MAT还可以将直方图中的类按照超类、类加载器或者包名分组。

当选中某个类时,MAT界面左上角的Inspector窗口将展示该类的Class实例的相关信息,如类加载器等。(下图中的ClassLoader @ 0x0指的便是启动类加载器。)

支配树的概念源自图论。在一则流图(flow diagram)中,如果从入口节点到b节点的所有路径都要经过a节点,那么a支配(dominate)b。

在a支配b,且a不同于b的情况下(即a严格支配b),如果从a节点到b节点的所有路径中不存在支配b的其他节点,那么a直接支配(immediate dominate)b。这里的支配树指的便是由节点的直接支配节点所组成的树状结构。

我们可以将堆中所有的对象看成一张对象图,每个对象是一个图节点,而GC Roots则是对象图的入口,对象之间的引用关系则构成了对象图中的有向边。这样一来,我们便能够构造出该对象图所对应的支配树。

MAT将按照每个对象Retained heap的大小排列该支配树。如下图所示:

根据Retained heap的定义,只要能够回收上图右侧的表中的第一个对象,那么垃圾回收器便能够释放出13.6MB内存。

需要注意的是,对象的引用型字段未必对应支配树中的父子节点关系。假设对象a拥有两个引用型字段,分别指向b和c。而b和c各自拥有一个引用型字段,但都指向d。如果没有其他引用指向b、c或d,那么a直接支配b、c和d,而b(或c)和d之间不存在支配关系。

当在支配树视图中选中某一对象时,我们还可以通过Path To GC Roots功能,反向列出该对象到GC Roots的引用路径。如下图所示:

MAT还将自动匹配内存泄漏中的常见模式,并汇报潜在的内存泄漏问题。具体可参考该帮助文档以及这篇博客

# Java Mission Control

注意:自Java 11开始,本节介绍的JFR已经开源。但在之前的Java版本,JFR属于Commercial Feature,需要通过Java虚拟机参数-XX:+UnlockCommercialFeatures开启。

我个人不清楚也不能回答关于Java 11之前的版本是否仍需要商务许可(Commercial License)的问题。请另行咨询后再使用,或者直接使用Java 11。

Java Mission Control(JMC)是Java虚拟机平台上的性能监控工具。它包含一个GUI客户端,以及众多用来收集Java虚拟机性能数据的插件,如JMX Console(能够访问用来存放虚拟机各个子系统运行数据的MXBeans),以及虚拟机内置的高效profiling工具Java Flight Recorder(JFR)。

JFR的性能开销很小,在默认配置下平均低于1%。与其他工具相比,JFR能够直接访问虚拟机内的数据,并且不会影响虚拟机的优化。因此,它非常适用于生产环境下满负荷运行的Java程序。

当启用时,JFR将记录运行过程中发生的一系列事件。其中包括Java层面的事件,如线程事件、锁事件,以及Java虚拟机内部的事件,如新建对象、垃圾回收和即时编译事件。

按照发生时机以及持续时间来划分,JFR的事件共有四种类型,它们分别为以下四种。

  1. 瞬时事件(Instant Event),用户关心的是它们发生与否,例如异常、线程启动事件。
  2. 持续事件(Duration Event),用户关心的是它们的持续时间,例如垃圾回收事件。
  3. 计时事件(Timed Event),是时长超出指定阈值的持续事件。
  4. 取样事件(Sample Event),是周期性取样的事件。
    取样事件的其中一个常见例子便是方法抽样(Method Sampling),即每隔一段时间统计各个线程的栈轨迹。如果在这些抽样取得的栈轨迹中存在一个反复出现的方法,那么我们可以推测该方法是热点方法。

JFR的取样事件要比其他工具更加精确。以方法抽样为例,其他工具通常基于JVMTI(Java Virtual Machine Tool Interface)的GetAllStackTraces API。该API依赖于安全点机制,其获得的栈轨迹总是在安全点上,由此得出的结论未必精确。JFR则不然,它不依赖于安全点机制,因此其结果相对来说更加精确。

JFR的启用方式主要有三种。

第一种是在运行目标Java程序时添加-XX:StartFlightRecording=参数。关于该参数的配置详情,你可以参考该帮助文档(请在页面中搜索StartFlightRecording)。

下面我列举三种常见的配置方式。

  • 在下面这条命令中,JFR将会在Java虚拟机启动5s后(对应delay=5s)收集数据,持续20s(对应duration=20s)。当收集完毕后,JFR会将收集得到的数据保存至指定的文件中(对应filename=myrecording.jfr)。

    # Time fixed

    $ java -XX:StartFlightRecording=delay=5s,duration=20s,filename=myrecording.jfr,settings=profile MyApp

settings=profile指定了JFR所收集的事件类型。默认情况下,JFR将加载配置文件$JDK/lib/jfr/default.jfc,并识别其中所包含的事件类型。当使用了settings=profile配置时,JFR将加载配置文件$JDK/lib/jfr/profile.jfc。该配置文件所包含的事件类型要多于默认的default.jfc,因此性能开销也要大一些(约为2%)。

default.jfc以及profile.jfc均为XML文件。后面我会介绍如何利用JMC来进行修改。

  • 在下面这条命令中,JFR将在Java虚拟机启动之后持续收集数据,直至进程退出。在进程退出时(对应dumponexit=true),JFR会将收集得到的数据保存至指定的文件中。

    # Continuous, dump on exit

    $ java -XX:StartFlightRecording=dumponexit=true,filename=myrecording.jfr MyApp

  • 在下面这条命令中,JFR将在Java虚拟机启动之后持续收集数据,直至进程退出。该命令不会主动保存JFR收集得到的数据。

    # Continuous, dump on demand

    $ java -XX:StartFlightRecording=maxage=10m,maxsize=100m,name=SomeLabel MyApp Started recording 1.

    Use jcmd 38502 JFR.dump name=SomeLabel filename=FILEPATH to copy recording data to file. ...

由于JFR将持续收集数据,如果不加以限制,那么JFR可能会填满硬盘的所有空间。因此,我们有必要对这种模式下所收集的数据进行限制。

在这条命令中,maxage=10m指的是仅保留10分钟以内的事件,maxsize=100m指的是仅保留100MB以内的事件。一旦所收集的事件达到其中任意一个限制,JFR便会开始清除不合规格的事件。

然而,为了保持较小的性能开销,JFR并不会频繁地校验这两个限制。因此,在实践过程中你往往会发现指定文件的大小超出限制,或者文件中所存储事件的时间超出限制。具体解释请参考这篇帖子

前面提到,该命令不会主动保存JFR收集得到的数据。用户需要运行jcmd <PID> JFR.dump命令方能保存。

这便是JFR的第二种启用方式,即通过jcmd来让JFR开始收集数据、停止收集数据,或者保存所收集的数据,对应的子命令分别为JFR.startJFR.stop,以及JFR.dump

JFR.start子命令所接收的配置及格式和-XX:StartFlightRecording=参数的类似。这些配置包括delaydurationsettingsmaxagemaxsize以及name。前几个参数我们都已经介绍过了,最后一个参数name就是一个标签,当同一进程中存在多个JFR数据收集操作时,我们可以通过该标签来辨别。

在启动目标进程时,我们不再添加-XX:StartFlightRecording=参数。在目标进程运行过程中,我们可以运行JFR.start子命令远程启用目标进程的JFR功能。具体用法如下所示:

$ jcmd <PID> JFR.start settings=profile maxage=10m maxsize=150m name=SomeLabel

上述命令运行过后,目标进程中的JFR已经开始收集数据。此时,我们可以通过下述命令来导出已经收集到的数据:

$ jcmd <PID> JFR.dump name=SomeLabel filename=myrecording.jfr

最后,我们可以通过下述命令关闭目标进程中的JFR:

$ jcmd <PID> JFR.stop name=SomeLabel

关于JFR.startJFR.dumpJFR.stop的其他用法,你可以参考该帮助文档

第三种启用JFR的方式则是JMC中的JFR插件。

在JMC GUI客户端左侧的JVM浏览器中,我们可以看到所有正在运行的Java程序。当点击右键弹出菜单中的Start Flight Recording...时,JMC便会弹出另一个窗口,用来配置JFR的启动参数,如下图所示:

这里的配置参数与前两种启动JFR的方式并无二致,同样也包括标签名、收集数据的持续时间、缓存事件的时间及空间限制,以及配置所要监控事件的Event settings
(这里对应前两种启动方式的settings=default|profile

JMC提供了两个选择:Continuous和Profiling,分别对应$JDK/lib/jfr/里的default.jfcprofile.jfc

我们可以通过JMC的Flight Recording Template Manager导入这些jfc文件,并在GUI界面上进行更改。更改完毕后,我们可以导出为新的jfc文件,以便在服务器端使用。

当收集完成时,JMC会自动打开所生成的jfr文件,并在主界面中列举目标进程在收集数据的这段时间内的潜在问题。例如,Parallel Threads一节,便汇报了没有完整利用CPU资源的问题。

客户端的左边则罗列了Java虚拟机的各个子系统。JMC将根据JFR所收集到的每个子系统的事件来进行可视化,转换成图或者表。

这里我简单地介绍其中两个。

垃圾回收子系统所对应的选项卡展示了JFR所收集到的GC事件,以及基于这些GC事件的数据生成的堆已用空间的分布图,Metaspace大小的分布图,最长暂停以及总暂停的直方分布图。

即时编译子系统所对应的选项卡则展示了方法编译时间的直方图,以及按编译时间排序的编译任务表。

后者可能出现同方法名同方法描述符的编译任务。其原因主要有两个,一是不同编译层次的即时编译,如3层的C1编译以及4层的C2编译。二是去优化后的重新编译。

JMC的图表总体而言都不难理解。你可以逐个探索,我在这里便不详细展开了。

# 总结与实践

今天我介绍了两个GUI工具:eclipse MAT以及JMC。

eclipse MAT可用于分析由jmap命令导出的Java堆快照。它包括两个相对比较重要的视图,分别为直方图和支配树。直方图展示了各个类的实例数目以及这些实例的Shallow heap或Retained heap的总和。支配树则展示了快照中每个对象所直接支配的对象。

Java Mission Control是Java虚拟机平台上的性能监控工具。Java Flight Recorder是JMC的其中一个组件,能够以极低的性能开销收集Java虚拟机的性能数据。

JFR的启用方式有三种,分别为在命令行中使用-XX:StartFlightRecording=参数,使用jcmdJFR.*子命令,以及JMC的JFR插件。JMC能够加载JFR的输出结果,并且生成各种信息丰富的图表。


今天的实践环节,请你试用JMC中的MBean Server功能,并通过JMC的帮助文档(Help->Java Mission Control Help),以及该教程来了解该功能的具体含义。

由于篇幅的限制,我就不再介绍VisualVM 以及JITWatch 了。感兴趣的同学可自行下载研究。