learn.lianglianglee.com/专栏/深入拆解Java虚拟机/31 Java虚拟机的监控及诊断工具(GUI篇).md.html
2022-05-11 18:57:05 +08:00

1039 lines
31 KiB
HTML
Raw Permalink 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.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>31 Java虚拟机的监控及诊断工具GUI篇.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/深入拆解Java虚拟机/00 开篇词 为什么我们要学习Java虚拟机.md.html">00 开篇词 为什么我们要学习Java虚拟机.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/01 Java代码是怎么运行的.md.html">01 Java代码是怎么运行的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/02 Java的基本类型.md.html">02 Java的基本类型.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/03 Java虚拟机是如何加载Java类的.md.html">03 Java虚拟机是如何加载Java类的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/04 JVM是如何执行方法调用的.md.html">04 JVM是如何执行方法调用的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/05 JVM是如何执行方法调用的.md.html">05 JVM是如何执行方法调用的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/06 JVM是如何处理异常的.md.html">06 JVM是如何处理异常的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/07 JVM是如何实现反射的.md.html">07 JVM是如何实现反射的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/08 JVM是怎么实现invokedynamic的.md.html">08 JVM是怎么实现invokedynamic的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/09 JVM是怎么实现invokedynamic的.md.html">09 JVM是怎么实现invokedynamic的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/10 Java对象的内存布局.md.html">10 Java对象的内存布局.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/11 垃圾回收(上).md.html">11 垃圾回收(上).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/12 垃圾回收(下).md.html">12 垃圾回收(下).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/13 Java内存模型.md.html">13 Java内存模型.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/14 Java虚拟机是怎么实现synchronized的.md.html">14 Java虚拟机是怎么实现synchronized的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/15 Java语法糖与Java编译器.md.html">15 Java语法糖与Java编译器.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/16 即时编译(上).md.html">16 即时编译(上).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/17 即时编译(下).md.html">17 即时编译(下).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/18 即时编译器的中间表达形式.md.html">18 即时编译器的中间表达形式.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/19 Java字节码基础篇.md.html">19 Java字节码基础篇.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/20 方法内联(上).md.html">20 方法内联(上).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/21 方法内联(下).md.html">21 方法内联(下).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/22 HotSpot虚拟机的intrinsic.md.html">22 HotSpot虚拟机的intrinsic.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/23 逃逸分析.md.html">23 逃逸分析.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/24 字段访问相关优化.md.html">24 字段访问相关优化.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/25 循环优化.md.html">25 循环优化.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/26 向量化.md.html">26 向量化.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/27 注解处理器.md.html">27 注解处理器.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/28 基准测试框架JMH.md.html">28 基准测试框架JMH.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/29 基准测试框架JMH.md.html">29 基准测试框架JMH.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/30 Java虚拟机的监控及诊断工具命令行篇.md.html">30 Java虚拟机的监控及诊断工具命令行篇.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/深入拆解Java虚拟机/31 Java虚拟机的监控及诊断工具GUI篇.md.html">31 Java虚拟机的监控及诊断工具GUI篇.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/32 JNI的运行机制.md.html">32 JNI的运行机制.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/33 Java Agent与字节码注入.md.html">33 Java Agent与字节码注入.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/34 Graal用Java编译Java.md.html">34 Graal用Java编译Java.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/35 Truffle语言实现框架.md.html">35 Truffle语言实现框架.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/36 SubstrateVMAOT编译框架.md.html">36 SubstrateVMAOT编译框架.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/尾声丨道阻且长,努力加餐.html.md.html">尾声丨道阻且长,努力加餐.html.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/工具篇 常用工具介绍.md.html">工具篇 常用工具介绍.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>31 Java虚拟机的监控及诊断工具GUI篇</h1>
<p>今天我们来继续了解 Java 虚拟机的监控及诊断工具。</p>
<h2>eclipse MAT</h2>
<p>在上一篇中,我介绍了<code>jmap</code>工具,它支持导出 Java 虚拟机堆的二进制快照。eclipse 的<a href="https://www.eclipse.org/mat/">MAT 工具</a>便是其中一个能够解析这类二进制快照的工具。</p>
<p>MAT 本身也能够获取堆的二进制快照。该功能将借助<code>jps</code>列出当前正在运行的 Java 进程,以供选择并获取快照。由于<code>jps</code>会将自己列入其中,因此你会在列表中发现一个已经结束运行的<code>jps</code>进程。</p>
<p><img src="assets/c9072149fb112312cbc217acc2660c7e.png" alt="img" /></p>
<p>MAT 获取二进制快照的方式有三种,一是使用 Attach API二是新建一个 Java 虚拟机来运行 Attach API三是使用<code>jmap</code>工具。</p>
<p>这三种本质上都是在使用 Attach API。不过在目标进程启用了<code>DisableAttachMechanism</code>参数时,前两者将不在选取列表中显示,后者将在运行时报错。</p>
<p>当加载完堆快照之后MAT 的主界面将展示一张饼状图,其中列举占据的 Retained heap 最多的几个对象。</p>
<p><img src="assets/da2e5894d0be535b6daa5084beb33ebf.png" alt="img" /></p>
<p>这里讲一下 MAT 计算对象占据内存的<a href="https://help.eclipse.org/mars/topic/org.eclipse.mat.ui.help/concepts/shallowretainedheap.html?cp=46_2_1">两种方式</a>。第一种是 Shallow heap指的是对象自身所占据的内存。第二种是 Retained heap指的是当对象不再被引用时垃圾回收器所能回收的总内存包括对象自身所占据的内存以及仅能够通过该对象引用到的其他对象所占据的内存。上面的饼状图便是基于 Retained heap 的。</p>
<p>MAT 包括了两个比较重要的视图分别是直方图histogram和支配树dominator tree</p>
<p><img src="assets/bbb59ca4d86c227dac23f30c360c9405.png" alt="img" /></p>
<p>MAT 的直方图和<code>jmap</code><code>-histo</code>子命令一样,都能够展示各个类的实例数目以及这些实例的 Shallow heap 总和。但是MAT 的直方图还能够计算 Retained heap并支持基于实例数目或 Retained heap 的排序方式(默认为 Shallow heap。此外MAT 还可以将直方图中的类按照超类、类加载器或者包名分组。</p>
<p>当选中某个类时MAT 界面左上角的 Inspector 窗口将展示该类的 Class 实例的相关信息,如类加载器等。(下图中的<code>ClassLoader @ 0x0</code>指的便是启动类加载器。)</p>
<p><img src="assets/dde7022060fad3945944fb7e4c9926ab.png" alt="img" /></p>
<p>支配树的概念源自图论。在一则流图flow diagram如果从入口节点到 b 节点的所有路径都要经过 a 节点,那么 a 支配dominateb。</p>
<p>在 a 支配 b且 a 不同于 b 的情况下(即 a 严格支配 b如果从 a 节点到 b 节点的所有路径中不存在支配 b 的其他节点,那么 a 直接支配immediate dominateb。这里的支配树指的便是由节点的直接支配节点所组成的树状结构。</p>
<p>我们可以将堆中所有的对象看成一张对象图,每个对象是一个图节点,而 GC Roots 则是对象图的入口,对象之间的引用关系则构成了对象图中的有向边。这样一来,我们便能够构造出该对象图所对应的支配树。</p>
<p>MAT 将按照每个对象 Retained heap 的大小排列该支配树。如下图所示:</p>
<p><img src="assets/0d4ea7f00d500db8a978ff0183a840a6.png" alt="img" /></p>
<p>根据 Retained heap 的定义,只要能够回收上图右侧的表中的第一个对象,那么垃圾回收器便能够释放出 13.6MB 内存。</p>
<p>需要注意的是,对象的引用型字段未必对应支配树中的父子节点关系。假设对象 a 拥有两个引用型字段,分别指向 b 和 c。而 b 和 c 各自拥有一个引用型字段,但都指向 d。如果没有其他引用指向 b、c 或 d那么 a 直接支配 b、c 和 d而 b或 c和 d 之间不存在支配关系。</p>
<p>当在支配树视图中选中某一对象时,我们还可以通过 Path To GC Roots 功能,反向列出该对象到 GC Roots 的引用路径。如下图所示:</p>
<p><img src="assets/e04d55d955832bf681aba16dcffc2ee7.png" alt="img" /></p>
<p>MAT 还将自动匹配内存泄漏中的常见模式,并汇报潜在的内存泄漏问题。具体可参考该<a href="https://help.eclipse.org/mars/topic/org.eclipse.mat.ui.help/tasks/runningleaksuspectreport.html?cp=46_3_1">帮助文档</a>以及<a href="https://memoryanalyzer.blogspot.com/2008/05/automated-heap-dump-analysis-finding.html">这篇博客</a></p>
<h2>Java Mission Control</h2>
<blockquote>
<p>注意:自 Java 11 开始,本节介绍的 JFR 已经开源。但在之前的 Java 版本JFR 属于 Commercial Feature需要通过 Java 虚拟机参数<code>-XX:+UnlockCommercialFeatures</code>开启。</p>
<p>我个人不清楚也不能回答关于 Java 11 之前的版本是否仍需要商务许可Commercial License的问题。请另行咨询后再使用或者直接使用 Java 11。</p>
<p><a href="https://jdk.java.net/jmc/">Java Mission Control</a>JMC是 Java 虚拟机平台上的性能监控工具。它包含一个 GUI 客户端,以及众多用来收集 Java 虚拟机性能数据的插件,如 JMX Console能够访问用来存放虚拟机各个子系统运行数据的<a href="https://en.wikipedia.org/wiki/Java_Management_Extensions#Managed_beans">MXBeans</a>),以及虚拟机内置的高效 profiling 工具 Java Flight RecorderJFR</p>
</blockquote>
<p>JFR 的性能开销很小,在默认配置下平均低于 1%。与其他工具相比JFR 能够直接访问虚拟机内的数据,并且不会影响虚拟机的优化。因此,它非常适用于生产环境下满负荷运行的 Java 程序。</p>
<p>当启用时JFR 将记录运行过程中发生的一系列事件。其中包括 Java 层面的事件,如线程事件、锁事件,以及 Java 虚拟机内部的事件,如新建对象、垃圾回收和即时编译事件。</p>
<p>按照发生时机以及持续时间来划分JFR 的事件共有四种类型,它们分别为以下四种。</p>
<ol>
<li>瞬时事件Instant Event用户关心的是它们发生与否例如异常、线程启动事件。</li>
<li>持续事件Duration Event用户关心的是它们的持续时间例如垃圾回收事件。</li>
<li>计时事件Timed Event是时长超出指定阈值的持续事件。</li>
<li>取样事件Sample Event是周期性取样的事件。
取样事件的其中一个常见例子便是方法抽样Method Sampling即每隔一段时间统计各个线程的栈轨迹。如果在这些抽样取得的栈轨迹中存在一个反复出现的方法那么我们可以推测该方法是热点方法。</li>
</ol>
<p>JFR 的取样事件要比其他工具更加精确。以方法抽样为例,其他工具通常基于 JVMTI<a href="https://docs.oracle.com/en/java/javase/11/docs/specs/jvmti.html">Java Virtual Machine Tool Interface</a>)的<code>GetAllStackTraces</code> API。该 API 依赖于安全点机制其获得的栈轨迹总是在安全点上由此得出的结论未必精确。JFR 则不然,它不依赖于安全点机制,因此其结果相对来说更加精确。</p>
<p>JFR 的启用方式主要有三种。</p>
<p>第一种是在运行目标 Java 程序时添加<code>-XX:StartFlightRecording=</code>参数。关于该参数的配置详情,你可以参考<a href="https://docs.oracle.com/en/java/javase/11/tools/java.html">该帮助文档</a>(请在页面中搜索<code>StartFlightRecording</code>)。</p>
<p>下面我列举三种常见的配置方式。</p>
<ul>
<li>在下面这条命令中JFR 将会在 Java 虚拟机启动 5s 后(对应<code>delay=5s</code>)收集数据,持续 20s对应<code>duration=20s</code>。当收集完毕后JFR 会将收集得到的数据保存至指定的文件中(对应<code>filename=myrecording.jfr</code>)。</li>
</ul>
<pre><code># Time fixed
$ java -XX:StartFlightRecording=delay=5s,duration=20s,filename=myrecording.jfr,settings=profile MyApp
</code></pre>
<blockquote>
<p><code>settings=profile</code>指定了 JFR 所收集的事件类型。默认情况下JFR 将加载配置文件<code>$JDK/lib/jfr/default.jfc</code>,并识别其中所包含的事件类型。当使用了<code>settings=profile</code>配置时JFR 将加载配置文件<code>$JDK/lib/jfr/profile.jfc</code>。该配置文件所包含的事件类型要多于默认的<code>default.jfc</code>,因此性能开销也要大一些(约为 2%)。</p>
<p><code>default.jfc</code>以及<code>profile.jfc</code>均为 XML 文件。后面我会介绍如何利用 JMC 来进行修改。</p>
</blockquote>
<ul>
<li>在下面这条命令中JFR 将在 Java 虚拟机启动之后持续收集数据,直至进程退出。在进程退出时(对应<code>dumponexit=true</code>JFR 会将收集得到的数据保存至指定的文件中。</li>
</ul>
<pre><code># Continuous, dump on exit
$ java -XX:StartFlightRecording=dumponexit=true,filename=myrecording.jfr MyApp
</code></pre>
<ul>
<li>在下面这条命令中JFR 将在 Java 虚拟机启动之后持续收集数据,直至进程退出。该命令不会主动保存 JFR 收集得到的数据。</li>
</ul>
<pre><code># 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.
...
</code></pre>
<p>由于 JFR 将持续收集数据,如果不加以限制,那么 JFR 可能会填满硬盘的所有空间。因此,我们有必要对这种模式下所收集的数据进行限制。</p>
<p>在这条命令中,<code>maxage=10m</code>指的是仅保留 10 分钟以内的事件,<code>maxsize=100m</code>指的是仅保留 100MB 以内的事件。一旦所收集的事件达到其中任意一个限制JFR 便会开始清除不合规格的事件。</p>
<p>然而为了保持较小的性能开销JFR 并不会频繁地校验这两个限制。因此,在实践过程中你往往会发现指定文件的大小超出限制,或者文件中所存储事件的时间超出限制。具体解释请参考<a href="https://community.oracle.com/thread/3514679">这篇帖子</a></p>
<p>前面提到,该命令不会主动保存 JFR 收集得到的数据。用户需要运行<code>jcmd &lt;PID&gt; JFR.dump</code>命令方能保存。</p>
<p>这便是 JFR 的第二种启用方式,即通过<code>jcmd</code>来让 JFR 开始收集数据、停止收集数据,或者保存所收集的数据,对应的子命令分别为<code>JFR.start</code><code>JFR.stop</code>,以及<code>JFR.dump</code></p>
<p><code>JFR.start</code>子命令所接收的配置及格式和<code>-XX:StartFlightRecording=</code>参数的类似。这些配置包括<code>delay</code><code>duration</code><code>settings</code><code>maxage</code><code>maxsize</code>以及<code>name</code>。前几个参数我们都已经介绍过了,最后一个参数<code>name</code>就是一个标签,当同一进程中存在多个 JFR 数据收集操作时,我们可以通过该标签来辨别。</p>
<p>在启动目标进程时,我们不再添加<code>-XX:StartFlightRecording=</code>参数。在目标进程运行过程中,我们可以运行<code>JFR.start</code>子命令远程启用目标进程的 JFR 功能。具体用法如下所示:</p>
<pre><code>$ jcmd &lt;PID&gt; JFR.start settings=profile maxage=10m maxsize=150m name=SomeLabel
</code></pre>
<p>上述命令运行过后,目标进程中的 JFR 已经开始收集数据。此时,我们可以通过下述命令来导出已经收集到的数据:</p>
<pre><code>$ jcmd &lt;PID&gt; JFR.dump name=SomeLabel filename=myrecording.jfr
</code></pre>
<p>最后,我们可以通过下述命令关闭目标进程中的 JFR</p>
<pre><code>$ jcmd &lt;PID&gt; JFR.stop name=SomeLabel
</code></pre>
<p>关于<code>JFR.start</code><code>JFR.dump</code><code>JFR.stop</code>的其他用法,你可以参考<a href="https://docs.oracle.com/javacomponents/jmc-5-5/jfr-runtime-guide/comline.htm#JFRRT185">该帮助文档</a></p>
<p>第三种启用 JFR 的方式则是 JMC 中的 JFR 插件。</p>
<p><img src="assets/395900f606fd93570196a6dcbac75e16.png" alt="img" /></p>
<p>在 JMC GUI 客户端左侧的 JVM 浏览器中,我们可以看到所有正在运行的 Java 程序。当点击右键弹出菜单中的<code>Start Flight Recording...</code>JMC 便会弹出另一个窗口,用来配置 JFR 的启动参数,如下图所示:</p>
<p><img src="assets/31f86bc1cafc569f51e0364d716cab6a.png" alt="img" /></p>
<p>这里的配置参数与前两种启动 JFR 的方式并无二致,同样也包括标签名、收集数据的持续时间、缓存事件的时间及空间限制,以及配置所要监控事件的<code>Event settings</code>
(这里对应前两种启动方式的<code>settings=default|profile</code></p>
<blockquote>
<p>JMC 提供了两个选择Continuous 和 Profiling分别对应<code>$JDK/lib/jfr/</code>里的<code>default.jfc</code><code>profile.jfc</code></p>
</blockquote>
<p>我们可以通过 JMC 的<code>Flight Recording Template Manager</code>导入这些 jfc 文件,并在 GUI 界面上进行更改。更改完毕后,我们可以导出为新的 jfc 文件,以便在服务器端使用。</p>
<p>当收集完成时JMC 会自动打开所生成的 jfr 文件,并在主界面中列举目标进程在收集数据的这段时间内的潜在问题。例如,<code>Parallel Threads</code>一节,便汇报了没有完整利用 CPU 资源的问题。</p>
<p><img src="assets/5a4302c29947518250e2b697aecc8d7c.png" alt="img" /></p>
<p>客户端的左边则罗列了 Java 虚拟机的各个子系统。JMC 将根据 JFR 所收集到的每个子系统的事件来进行可视化,转换成图或者表。</p>
<p><img src="assets/dbc36a8713af058c79df2878379276ff.png" alt="img" /></p>
<p>这里我简单地介绍其中两个。</p>
<p>垃圾回收子系统所对应的选项卡展示了 JFR 所收集到的 GC 事件,以及基于这些 GC 事件的数据生成的堆已用空间的分布图Metaspace 大小的分布图,最长暂停以及总暂停的直方分布图。</p>
<p><img src="assets/56f9fb2932ffb63ffa29e95dc779100c.png" alt="img" /></p>
<p>即时编译子系统所对应的选项卡则展示了方法编译时间的直方图,以及按编译时间排序的编译任务表。</p>
<p>后者可能出现同方法名同方法描述符的编译任务。其原因主要有两个,一是不同编译层次的即时编译,如 3 层的 C1 编译以及 4 层的 C2 编译。二是去优化后的重新编译。</p>
<p><img src="assets/6e7e41a6f8945a2b65d67c18ea5293c8.png" alt="img" /></p>
<p>JMC 的图表总体而言都不难理解。你可以逐个探索,我在这里便不详细展开了。</p>
<h2>总结与实践</h2>
<p>今天我介绍了两个 GUI 工具eclipse MAT 以及 JMC。</p>
<p>eclipse MAT 可用于分析由<code>jmap</code>命令导出的 Java 堆快照。它包括两个相对比较重要的视图,分别为直方图和支配树。直方图展示了各个类的实例数目以及这些实例的 Shallow heap 或 Retained heap 的总和。支配树则展示了快照中每个对象所直接支配的对象。</p>
<p>Java Mission Control 是 Java 虚拟机平台上的性能监控工具。Java Flight Recorder 是 JMC 的其中一个组件,能够以极低的性能开销收集 Java 虚拟机的性能数据。</p>
<p>JFR 的启用方式有三种,分别为在命令行中使用<code>-XX:StartFlightRecording=</code>参数,使用<code>jcmd</code><code>JFR.*</code>子命令,以及 JMC 的 JFR 插件。JMC 能够加载 JFR 的输出结果,并且生成各种信息丰富的图表。</p>
<hr />
<p>今天的实践环节,请你试用 JMC 中的 MBean Server 功能,并通过 JMC 的帮助文档(<code>Help-&gt;Java Mission Control Help</code>),以及<a href="https://docs.oracle.com/javase/tutorial/jmx/mbeans/index.html">该教程</a>来了解该功能的具体含义。</p>
<p><img src="assets/2a68f0f2b5de35f29b045fe82923ac7f.png" alt="img" /></p>
<p>由于篇幅的限制,我就不再介绍<a href="https://visualvm.github.io/index.html">VisualVM</a> 以及<a href="https://github.com/AdoptOpenJDK/jitwatch">JITWatch</a> 了。感兴趣的同学可自行下载研究。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/深入拆解Java虚拟机/30 Java虚拟机的监控及诊断工具命令行篇.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/深入拆解Java虚拟机/32 JNI的运行机制.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997a1ec90e3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>