3. JVM监控及诊断工具-GUI篇
3.1 工具概述
使用上一章命令行工具或组合能帮您获取目标Java应用性能相关的基础信息,但它们存在下列局限:
1.无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等(这对定位应用性能瓶颈至关重要)。
2.要求用户登录到目标 Java 应用所在的宿主机上,使用起来不是很方便。
3.分析数据通过终端输出,结果展示不够直观。
为此,JDK提供了一些内存泄漏的分析工具,如jconsole,jvisualvm等,用于辅助开发人员定位问题,但是这些工具很多时候并不足以满足快速定位的需求。所以这里我们介绍的工具相对多一些、丰富一些。
图形化综合诊断工具
- JDK自带的工具
- jconsole:JDK 自带的可视化监控工具。查看 Java 应用程序的运行概况、监控堆信息、永久区(或元空间)使用情况、类加载情况等
- 位置:
<JAVA_HOME>\bin\jconsole.exe
- 位置:
- Visual VM:Visual VM 是一个工具,它提供了一个可视界面,用于查看 Java 虚拟机上运行的基于 Java 技术的应用程序的详细信息。
- 官网:https://visualvm.github.io/ ,默认情况下,JDK 不再带这个工具,需要自行下载
- JMC:Java Mission Control,内置 Java Flight Recorder。能够以极低的性能开销收集Java虚拟机的性能数据。
- jconsole:JDK 自带的可视化监控工具。查看 Java 应用程序的运行概况、监控堆信息、永久区(或元空间)使用情况、类加载情况等
- 第三方工具
- MAT:MAT(Memory Analyzer Tool)是基于 Eclipse 的内存分析工具,是一个快速、功能丰富的 Java heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
- Eclipse 的插件形式安装
- JProfiler:商业软件,需要付费。功能强大。
- MAT:MAT(Memory Analyzer Tool)是基于 Eclipse 的内存分析工具,是一个快速、功能丰富的 Java heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
3.2 JConsole
jconsole:从 Java 5 开始,在 JDK 中自带的 Java 监控和管理控制台。用于对 JVM 中内存、线程和类等的监控,是一个基于 JMX(java management extensions) 的 GUI 性能监控工具。
官方文档:https://docs.oracle.com/javase/7/docs/technotes/guides/management/jconsole.html
3.2.1 启动
两种启动方式:
- 直接单击
jconsole.exe,仅适用于 Windows - 命令行窗口中输入
jconsole命令
启动界面如下:

需要选择一个要监控的进程。
界面如下:



三种启动方式
- Local:使用 JConsole 连接一个正在本地系统运行的 JVM,并且执行程序的和运行 JConsole 的需要是同一个用户。JConsole 使用文件系统的授权通过 RMI 连接起链接到平台的 MBean 的服务器上。这种从本地连接的监控能力只有 Sun 的 JDK 具有。
- Remote:使用下面的 URL 通过 RMI 连接器连接到一个 JMX 代理,
service:jmx:rmi:///jndi/rmi://hostName:portNum/jmxrmi。JConsole 为建立连接,需要在环境变量中设置mx.remote.credentials来指定用户名和密码,从而进行授权。 - Advanced:使用一个特殊的 URL 连接 JMX 代理。一般情况使用自己定制的连接器而不是 RMI 提供的连接器来连接 JMX 代理,或者是一个使用 JDK1.4 的实现了 JMX 和 JMX Remote 的应用
3.3 Visual VM
- Visual VM是一个功能强大的多合一故障诊断和性能监控的可视化工具。
- 它集成了多个JDK命令行工具,使用Visual VM可用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)等,甚至代替JConsole。
- 在JDK 6 Update 7以后,Visual VM便作为JDK的一部分发布(VisualVM 在JDK/bin目录下)即:它完全免费。
- 此外,Visual VM也可以作为独立的软件安装:官方地址:https://visualvm.github.io/index.html

3.3.1 插件的安装
IDEA 安装 VisualVM Launcher 插件
"File" "Settings" "Plugins" 打开插件安装界面。
在市场(Marketplace)中搜索 “VisualVM Launcher”

插件安装好之后,重启IDEA,然后再打开 "Settings" 配置界面,找到 "VisualVM Launcher",配置VisualVM的路径和JDK,如下:

3.3.2 连接方式
1. 在IDEA中通过插件启动

两种启动方式,一种是正常启动,一种是DEBUG启动。
也可启动应用后通过,左侧的 “Start VisualVM” 来启动。

启动后的界面如下:


2. 本地连接
进入到VisualVM安装目录,然后进入到 "bin" 文件夹,单击 "visualvm.exe" 启动 VisualVM。
界面如下

在左侧的“Local” 下双击要监控的进程。即可进入监控界面

3. 远程连接

3.3.3 主要功能
- 生成/读取堆内存快照
- 查看JVM参数和系统属性
- 查看运行中的虚拟机进程
- 生成/读取线程快照
- 程序资源的实时监控
- 其他功能
- JMX代理连接
- 远程环境监控
- CPU分析和内存分析
生成堆内存/线程快照
在左侧的 “Local” 中通过进程生成堆内存/线程快照
在左侧的 “Local” 中右击要生成快照的进程
- 选择 “Heap Dump” 生成堆内存快照
- 选择 “Thread Dump” 生成线程快照

通过监视(Monitor)生成堆内存快照

堆内存快照如下:

这些快照存储在内存中,当线程停止的时候快照就会丢失,如果还想利用,可以将快照进行另存为操作,如下图:

通过线程(Threads)生成线程快照

线程快照如下:

这些快照存储在内存中,当线程停止的时候快照就会丢失,如果还想利用,可以将快照进行另存为操作,如下图:

载入堆内存/线程快照

通过 "File" "Load" 打开选择快照对话框。

选择堆内存快照或线程快照即可装入。
3.4 Eclipse MAT
3.4.1 概述
MAT(Memory Analyzer Tool)工具是一款功能强大的Java堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。
MAT是基于Eclipse开发的,不仅可以单独使用,还可以作为插件的形式嵌入在Eclipse中使用。是一款免费的性能分析工具,使用起来非常方便。大家可以在 https://www.eclipse.org/mat/downloads.php 下载并使用MAT

只要确保机器上装有 JDK 并配置好相关的环境变量,MAT 可正常启动。还可以在 Eclipse 中以插件的方式安装。

3.4.2 获取堆dump文件
MAT可以分析heap dump文件。在进行内存分析时,只要获得了反映当前设备内存映像的hprof文件,通过MAT打开就可以直观地看到当前的内存信 息。一般说来,这些内存信息包含:
- 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。
- 所有的类信息,包括ClassLoader、类名称、父类、静态变量等
- GCRoot到所有的这些对象的引用路径
- 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)
MAT 不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如Sun,HP,SAP 所采用的 HPROF 二进制堆存储文件,以及 IBM的 PHD 堆存储文件等都能被很好的解析。
最吸引人的还是能够快速为开发人员生成内存泄漏报表,方便定位问题和分析问题。虽然MAT有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从MAT展现给我们的信息当中通过经验和直觉来判断才能发现。

获取Dump文件
方式一:通过前一章介绍的jmap工具生成,可以生成任意一个 Java 进程的 dump 文件。
方式二:通过配置 JVM 参数生成
- 选项"
-XX:+HeapDumpOnOutOfMemoryError"或"-XX:+HeapDumpBeforeFullGC" - 选项"
-XX:HeapDumpPath"所代表的含义就是当程序出现0utOfMemory时,将会在相应的目录下 生成一份 dump 文件。如果不指定选项“-XX:HeapDumpPath" 则在当前目录下生成 dump 文件。
对比:考虑到生产环境中几乎不可能在线对其进行分析,大都是采用离线分析,因此使用 jmap+MAT 工具是最常见的组合。
方式三: 使用VisualVM可以导出堆dump文件
方式四: 使用MAT既可以打开一个已有的堆快照,也可以通过MAT直接从活动Java程序中导出堆快照。 该功能将借助jps列出当前正在运行的Java 进程,以供选择并获取快照。
3.4.3 分析堆 dump 文件

Histogram
展示了各个类的实例数目以及这些实例的 Shallow heap 或者 Retained heap 的总和

thread overview
- 查看系统中的Java线程
- 查看局部变量的信息


获取对象互相引用的关系
with outgoing references(对象的引出) 和 with incoming reference(对象的引入)

list objects

浅堆与深堆
MAT 计算对象占据内存的两种方式。shallow heap和retained heap。
浅堆(shallow heap)
浅堆 (Shallow Heap) 是指一个对象所消耗的内存。在 32 位系统中,一个对象引用会占据 4 个字节,一个 int 类型会占据 4 个字节,long 型变量会占据 8 个字节,每个对象头需要占用 8 个字节。根据堆快照格式不同,对象的大小可能会向 8 字节进行对齐。
以 String 为例: 2 个 int 值共占 8 字节,对象引用占用 4 字节,对象头 8 字节,合计 20 字节,向 8 字节对齐,故占 24 字节。(jdk7中)

这 24 字节为 String 对象的浅堆大小。它与 String 的 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节。
深堆(retained heap)
保留集 (Retained Set):
对象A的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括对象 A 本身),即对象A的保留集可以被认为是只能通过对象 A 被直接或间接访问到的所有对象的集合。通俗地说,就是指仅被对象 A 所持有的对象的集合。
深堆 (Retained Heap):
深堆是指对象的保留集中所有的对象的浅堆大小之和。
注意:浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。
当前深堆大小 = 当前对象的浅堆大小 + 对象中所包含对象的深堆大小
补充:对象实际大小
另外一个常用的概念是对象的实际大小。这里,对象的实际大小定义为一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小。与深堆相比,似乎这个在日常开发中更为直观和被人接受,但实际上,这个概念和垃圾回收无关。
下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,不含 C 和 D,而 A 的实际大小为 A、C、D 三者之和。而 A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到,因此不在对象 A 的深堆范围内。
练习
看图理解 Retained Size
上图中,GC Roots 直接引用了 A 和 B 两个对象
- A 对象的 Retained Size = A 对象的 Shallow Size
- B 对象的 Retained Size = B 对象的 Shallow Size + C 对象的 Shallow Size
这里不包括 D 对象, 因为 D 对象被 GC Roots 直接引用。
如果 GC Roots 不引用 D 对象呢?
案例分析
import java.util.ArrayList;
import java.util.List;
/**
* 有一个学生浏览网页的记录程序,它将记录 每个学生访问过的网站地址。
* 它由三个部分组成:Student、WebPage和StudentTrace三个类
*
* -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=c:\code\student.hprof
*/
public class StudentTrace {
static List<WebPage> webpages = new ArrayList<WebPage>();
public static void createWebPages() {
for (int i = 0; i < 100; i++) {
WebPage wp = new WebPage();
wp.setUrl("http://www." + Integer.toString(i) + ".com");
wp.setContent(Integer.toString(i));
webpages.add(wp);
}
}
public static void main(String[] args) {
createWebPages();//创建了100个网页
//创建3个学生对象
Student st3 = new Student(3, "Tom");
Student st5 = new Student(5, "Jerry");
Student st7 = new Student(7, "Lily");
for (int i = 0; i < webpages.size(); i++) {
if (i % st3.getId() == 0)
st3.visit(webpages.get(i));
if (i % st5.getId() == 0)
st5.visit(webpages.get(i));
if (i % st7.getId() == 0)
st7.visit(webpages.get(i));
}
webpages.clear();
System.gc();
}
}
class Student {
private int id;
private String name;
private List<WebPage> history = new ArrayList<>();
public Student(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<WebPage> getHistory() {
return history;
}
public void setHistory(List<WebPage> history) {
this.history = history;
}
public void visit(WebPage wp) {
if (wp != null) {
history.add(wp);
}
}
}
class WebPage {
private String url;
private String content;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}

结论:
elementData 数组的浅堆是 80 个字节,而 elementData 数组中的所有 WebPage 对象的深堆之和是 1208 个字节,所以加在一起就是 elementData 数组的深堆之和,也就是 1288 个字节
解释:
我说“elementData 数组的浅堆是 80 个字节”,其中 15 个对象一共是 60 个字节,对象头 8 个字节,数组对象本身 4 个字节,这些的和是 72 个字节,然后总和要是 8 的倍数,所以“elementData 数组的浅堆是 80 个字节”
我说“WebPage 对象的深堆之和是 1208 个字节”,一共有 15 个对象,其中 0、21、42、63、84、35、70 不仅仅是 7 的倍数,还是 3 或者 5 的倍数,所以这几个数值对应的 i 不能计算在深堆之内,这 15 个对象中大多数的深堆是 152 个字节,但是 i 是 0 和 7 的那两个深堆是 144 个字节,所以 (13152+1442)-(6*152+144)=1208,所以这也印证了我上面的话,即“WebPage 对象的深堆之和是 1208 个字节”
因此“elementData 数组的浅堆 80 个字节”加上“WebPage 对象的深堆之和 1208 个字节”,正好是 1288 个字节,说明“elementData 数组的浅堆 1288 个字节”
支配树
支配树的概念源自图论。
MAT 提供了一个称为支配树的对象图。支配树视图对数据进行了归类,体现了对象实例间的支配关系。在对象引用图中,所有指向对象 B 的路径都经过对象 A ,则认为对象 A 支配了对象 B。如果对象A时离对象 B 最近的一个支配对象,则认为对象 A 为对象 B 的直接支配者,支配树是基于对象间的引用图所建立的,它有以下基本性质:
- 对象 A 的子树(所有被对象 A 支配的对象集合)表示对象 A 的保留集(retained set),即深堆。
- 如果对象 A 支配对象 B,那么对象 A 的直接支配者也支配对象 B。
- 支配树的边与对象引用图的边不直接对应。
如下图所示:左图表示对象引用图,右图表示左图所对应的支配树。对象 A 和 B 由根对象直接支配,由于在到对象 C 的路径中,可以经过 A,也可以经过 B,因此对象 C 的直接支配者也是根对象。对象 F 与对象 D 相互引用,因为到对象 F 的所有路径必然经过对象 D,因此,对象 D 时对象 F 的直接支配者。而到对象 D 的所有路径中,必然经过对象 C,即使是从对象 F 到对象 D 的引用,从根节点触发,也是经过对象 C 的,所以,对象 D 的直接支配者为对象 C。
同理,对象 E 支配对象 G,支配关系是可传递的,因为 C 支配 E,所以 C 也支配 G。到达对象 H 可以通过对象 D,也可以通过对象 E,因此对象 D 和 E 都不能支配对象 H,而经过对象 C 既可以到达 D 也可以到达 E,因此对象 C 为对象 H 的直接支配者。
跟随我一起来理解如何从“对象引用图 支配树”,首先需要理 解支配者(如果要到达对象 B,毕竟经过对象 A,那么对象 A 就是对象 B 的支配者,可以想到支配者大于等于 1),然后需要理解直接支配者(在支配者中距离对象 B 最近的对象 A 就是对象 B 的直接支配者,你要明白直接支配者不一定就是对象 B 的上一级,然后直接支配者只有一个),然后还需要理解支配树是怎么画的,其实支配树中的对象与对象之间的关系就是直接支配关系,也就是上一级是下一级的直接支配者,只要按照这样的方式来作图,肯定能从“对象引用图 支配树”
** 在 Eclipse MAT 工具中如何查看支配树:**
在 MAT 中,单击工具栏上的对象支配树按钮,可以打开对象支配树视图。

下图显示了对象支配树视图的一部分。该截图显示部分 Lily 学生的 history 队列的直接支配对象。即当 Lily 对象被回收,也会一并回收的所有对象。显然能被 3 或者 5 整除的网页不会出现在该列表中,因为它们同时被另外两名学生对象引用。
TODO 补充图片