gitbook/深入拆解Java虚拟机/docs/40821.md
2022-09-03 22:05:03 +08:00

202 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 31 | Java虚拟机的监控及诊断工具GUI篇
今天我们来继续了解Java虚拟机的监控及诊断工具。
## eclipse MAT
在上一篇中,我介绍了`jmap`工具它支持导出Java虚拟机堆的二进制快照。eclipse的[MAT工具](https://www.eclipse.org/mat/)便是其中一个能够解析这类二进制快照的工具。
MAT本身也能够获取堆的二进制快照。该功能将借助`jps`列出当前正在运行的Java进程以供选择并获取快照。由于`jps`会将自己列入其中,因此你会在列表中发现一个已经结束运行的`jps`进程。
![](https://static001.geekbang.org/resource/image/c9/7e/c9072149fb112312cbc217acc2660c7e.png)
MAT获取二进制快照的方式有三种一是使用Attach API二是新建一个Java虚拟机来运行Attach API三是使用`jmap`工具。
这三种本质上都是在使用Attach API。不过在目标进程启用了`DisableAttachMechanism`参数时,前两者将不在选取列表中显示,后者将在运行时报错。
当加载完堆快照之后MAT的主界面将展示一张饼状图其中列举占据的Retained heap最多的几个对象。
![](https://static001.geekbang.org/resource/image/da/bf/da2e5894d0be535b6daa5084beb33ebf.png)
这里讲一下MAT计算对象占据内存的[两种方式](https://help.eclipse.org/mars/topic/org.eclipse.mat.ui.help/concepts/shallowretainedheap.html?cp=46_2_1)。第一种是Shallow heap指的是对象自身所占据的内存。第二种是Retained heap指的是当对象不再被引用时垃圾回收器所能回收的总内存包括对象自身所占据的内存以及仅能够通过该对象引用到的其他对象所占据的内存。上面的饼状图便是基于Retained heap的。
MAT包括了两个比较重要的视图分别是直方图histogram和支配树dominator tree
![](https://static001.geekbang.org/resource/image/bb/05/bbb59ca4d86c227dac23f30c360c9405.png)
MAT的直方图和`jmap`的`-histo`子命令一样都能够展示各个类的实例数目以及这些实例的Shallow heap总和。但是MAT的直方图还能够计算Retained heap并支持基于实例数目或Retained heap的排序方式默认为Shallow heap。此外MAT还可以将直方图中的类按照超类、类加载器或者包名分组。
当选中某个类时MAT界面左上角的Inspector窗口将展示该类的Class实例的相关信息如类加载器等。下图中的`ClassLoader @ 0x0`指的便是启动类加载器。)
![](https://static001.geekbang.org/resource/image/dd/ab/dde7022060fad3945944fb7e4c9926ab.png)
支配树的概念源自图论。在一则流图flow diagram如果从入口节点到b节点的所有路径都要经过a节点那么a支配dominateb。
在a支配b且a不同于b的情况下即a严格支配b如果从a节点到b节点的所有路径中不存在支配b的其他节点那么a直接支配immediate dominateb。这里的支配树指的便是由节点的直接支配节点所组成的树状结构。
我们可以将堆中所有的对象看成一张对象图每个对象是一个图节点而GC Roots则是对象图的入口对象之间的引用关系则构成了对象图中的有向边。这样一来我们便能够构造出该对象图所对应的支配树。
MAT将按照每个对象Retained heap的大小排列该支配树。如下图所示
![](https://static001.geekbang.org/resource/image/0d/a6/0d4ea7f00d500db8a978ff0183a840a6.png)
根据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的引用路径。如下图所示
![](https://static001.geekbang.org/resource/image/e0/e7/e04d55d955832bf681aba16dcffc2ee7.png)
MAT还将自动匹配内存泄漏中的常见模式并汇报潜在的内存泄漏问题。具体可参考该[帮助文档](https://help.eclipse.org/mars/topic/org.eclipse.mat.ui.help/tasks/runningleaksuspectreport.html?cp=46_3_1)以及[这篇博客](http://memoryanalyzer.blogspot.com/2008/05/automated-heap-dump-analysis-finding.html)。
## Java Mission Control
> 注意自Java 11开始本节介绍的JFR已经开源。但在之前的Java版本JFR属于Commercial Feature需要通过Java虚拟机参数`-XX:+UnlockCommercialFeatures`开启。
>
> 我个人不清楚也不能回答关于Java 11之前的版本是否仍需要商务许可Commercial License的问题。请另行咨询后再使用或者直接使用Java 11。
>
> [Java Mission Control](http://jdk.java.net/jmc/)JMC是Java虚拟机平台上的性能监控工具。它包含一个GUI客户端以及众多用来收集Java虚拟机性能数据的插件如JMX Console能够访问用来存放虚拟机各个子系统运行数据的[MXBeans](https://en.wikipedia.org/wiki/Java_Management_Extensions#Managed_beans)以及虚拟机内置的高效profiling工具Java Flight RecorderJFR
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](https://docs.oracle.com/en/java/javase/11/docs/specs/jvmti.html))的`GetAllStackTraces` API。该API依赖于安全点机制其获得的栈轨迹总是在安全点上由此得出的结论未必精确。JFR则不然它不依赖于安全点机制因此其结果相对来说更加精确。
JFR的启用方式主要有三种。
第一种是在运行目标Java程序时添加`-XX:StartFlightRecording=`参数。关于该参数的配置详情,你可以参考[该帮助文档](https://docs.oracle.com/en/java/javase/11/tools/java.html)(请在页面中搜索`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并不会频繁地校验这两个限制。因此在实践过程中你往往会发现指定文件的大小超出限制或者文件中所存储事件的时间超出限制。具体解释请参考[这篇帖子](https://community.oracle.com/thread/3514679)。
前面提到该命令不会主动保存JFR收集得到的数据。用户需要运行`jcmd <PID> JFR.dump`命令方能保存。
这便是JFR的第二种启用方式即通过`jcmd`来让JFR开始收集数据、停止收集数据或者保存所收集的数据对应的子命令分别为`JFR.start``JFR.stop`,以及`JFR.dump`。
`JFR.start`子命令所接收的配置及格式和`-XX:StartFlightRecording=`参数的类似。这些配置包括`delay`、`duration`、`settings`、`maxage`、`maxsize`以及`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.start`、`JFR.dump`和`JFR.stop`的其他用法,你可以参考[该帮助文档](https://docs.oracle.com/javacomponents/jmc-5-5/jfr-runtime-guide/comline.htm#JFRRT185)。
第三种启用JFR的方式则是JMC中的JFR插件。
![](https://static001.geekbang.org/resource/image/39/16/395900f606fd93570196a6dcbac75e16.png)
在JMC GUI客户端左侧的JVM浏览器中我们可以看到所有正在运行的Java程序。当点击右键弹出菜单中的`Start Flight Recording...`时JMC便会弹出另一个窗口用来配置JFR的启动参数如下图所示
![](https://static001.geekbang.org/resource/image/31/6a/31f86bc1cafc569f51e0364d716cab6a.png)
这里的配置参数与前两种启动JFR的方式并无二致同样也包括标签名、收集数据的持续时间、缓存事件的时间及空间限制以及配置所要监控事件的`Event settings`。
(这里对应前两种启动方式的`settings=default|profile`
> JMC提供了两个选择Continuous和Profiling分别对应`$JDK/lib/jfr/`里的`default.jfc`和`profile.jfc`。
我们可以通过JMC的`Flight Recording Template Manager`导入这些jfc文件并在GUI界面上进行更改。更改完毕后我们可以导出为新的jfc文件以便在服务器端使用。
当收集完成时JMC会自动打开所生成的jfr文件并在主界面中列举目标进程在收集数据的这段时间内的潜在问题。例如`Parallel Threads`一节便汇报了没有完整利用CPU资源的问题。
![](https://static001.geekbang.org/resource/image/5a/7c/5a4302c29947518250e2b697aecc8d7c.png)
客户端的左边则罗列了Java虚拟机的各个子系统。JMC将根据JFR所收集到的每个子系统的事件来进行可视化转换成图或者表。
![](https://static001.geekbang.org/resource/image/db/ff/dbc36a8713af058c79df2878379276ff.png)
这里我简单地介绍其中两个。
垃圾回收子系统所对应的选项卡展示了JFR所收集到的GC事件以及基于这些GC事件的数据生成的堆已用空间的分布图Metaspace大小的分布图最长暂停以及总暂停的直方分布图。
![](https://static001.geekbang.org/resource/image/56/0c/56f9fb2932ffb63ffa29e95dc779100c.png)
即时编译子系统所对应的选项卡则展示了方法编译时间的直方图,以及按编译时间排序的编译任务表。
后者可能出现同方法名同方法描述符的编译任务。其原因主要有两个一是不同编译层次的即时编译如3层的C1编译以及4层的C2编译。二是去优化后的重新编译。
![](https://static001.geekbang.org/resource/image/6e/c8/6e7e41a6f8945a2b65d67c18ea5293c8.png)
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=`参数,使用`jcmd`的`JFR.*`子命令以及JMC的JFR插件。JMC能够加载JFR的输出结果并且生成各种信息丰富的图表。
* * *
今天的实践环节请你试用JMC中的MBean Server功能并通过JMC的帮助文档`Help->Java Mission Control Help`),以及[该教程](https://docs.oracle.com/javase/tutorial/jmx/mbeans/index.html)来了解该功能的具体含义。
![](https://static001.geekbang.org/resource/image/2a/7f/2a68f0f2b5de35f29b045fe82923ac7f.png)
由于篇幅的限制,我就不再介绍[VisualVM](https://visualvm.github.io/index.html) 以及[JITWatch](https://github.com/AdoptOpenJDK/jitwatch) 了。感兴趣的同学可自行下载研究。