jianshu.com/p/4ed3dd8b7b83
MAT是MemoryAnalyzerTool的简称,它是一款功能强大的Java堆内存分析器,可以用于查找内存泄漏以及查看内存消耗情况。MAT是
基于Eclipse开发的一款免费的性能分析工具,读者可以在
http://www.eclipse.org/mat/上下载并使用MAT。
在分析堆快照前,首先需要导出应用程序的堆快照。在本书前文中提到的jmap、JConsole和VisualVM等工具都可以用于获得Java应用程序的堆快照文件。此外,MAT本身也具有这个功能。
如图6.66所示,在File菜单中选择AcquireHeapDump命令,在弹出对话框的当前Java应用程序列表中选择要分析的应用程序即可,如图6.67所示。
除了直接在MAT中导出正在运行的应用程序堆快照外,也可以通过File菜单中的OpenHeapDump命令打开一个既存的堆快照文件。
注意:使用MAT既可以打开一个已有的堆快照,也可以直接从活动Java程序中导出堆快照。
图6.68所示为正常打开堆快照文件后的MAT界面。
在图6.68的右侧界面中显示了堆快照文件的大小、类、实例和ClassLoader的总数;饼图中显示了当前堆快照中最大的对象。将光标悬停在饼图中,可以在左侧的Inspector界面中查看该对象的详细信息。在饼图中单击,可以对选中的对象进行更多的操作。
单击工具栏上的柱状图按钮(如图6.69所示),可以查看当前堆的类信息,包括类的对象数量、浅堆(Shallow)大小和深堆(Retained)大小,如图6.70所示。
通过柱状图界面,可以查找引用选中对象的对象集合以及选中对象所引用的对象集合。如图6.71所示,选中java.util.Vector对象并右击,在弹出的右键菜单中选择Listobjects命令,弹出的withoutgoingreferences和withincomingreferences子命令分别表示查找java.util.Vector实例的引用对象,以及引用java.util.Vector实例的对象。
注意:通过MAT,可以根据对象间的引用关系对内存中的对象进行分析。
图6.72显示了选择withincomingreferences命令后的输出结果,展示了两个被主线程引用的java.util.Vector局部变量实例。
为了方便查看,柱状图还可以根据ClassLoader和包对类进行排序。图6.73显示了MAT的柱状图排序功能,以及一个按照包进行排序的柱状图输出命令。
图6.72 引用关系查询结果
浅堆(ShallowHeap)和深堆(RetainedHeap)是两个非常重要的概念,它们分别表示一个对象结构所占用的内存大小和一个对象被执行GC操作后,可以真实释放的内存大小。
浅堆是指一个对象所消耗的内存。在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8个字节。
根据堆快照格式不同,对象的大小可能会向8字节进行对齐。以String对象为例,图6.74显示了String对象的几个属性。
3个int类型以及一个引用类型合计占用的内存为3×4+4=16字节,再加上对象头的8个字节,因此String对象占用的空间,即浅堆的大小是16+8=24字节。浅堆的大小只与对象的结构有关,与对象的实际内容无关。也就是说,无论字符串的长度是多少,内容是什么,浅堆的大小始终是24字节。
深堆的概念略微复杂。要理解深堆,首先需要了解保留集(RetainedSet)。对象A的保留集指当对象A被垃圾回收后,可以被释放的所有的对象集合(包括对象A本身),即对象A的保留集可以被认为是只能通过对象A被直接或者间接访问到的所有对象的集合。通俗地说,就是指仅被对象A所持有的对象的集合。深堆是指对象的保留集中所有对象的浅堆之和。
注意:浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。
下面这个例子很好地诠释了深堆的概念。首先是表示点的类定义:
接着是表示线的类定义:
主函数构造了a、b、c、d、e、f、g这7个点,以及aLine、bLine、cLine和dLine这4条线,并在程序最后将a、b、c、d、e这5个点设置为null。具体代码如下:
这段代码的对象引用关系如图6.75所示,其中a、b、c、d、e对象在使用完成后被设置为null。
根据Point类的结构,一个Point实例的浅堆大小为4×2+8=16字节,一个Line实例的浅堆大小为4×2+8=16字节。使用MAT得到该示例的内存快照文件,如图6.76所示。为了阅读方便,笔者将代码中的变量名标识到了内存快照中的对象上。
可以看到,所有的Point实例浅堆和深堆的大小都是16字节。而dLine对象,浅堆为16字节,深堆也是16字节,这是因为dLine对象内的两个点f和g没有被设置为null,因此即使dLine被回收,f和g也不会被释放。对象cLine内的引用对象d和e由于仅在cLine内还存在引用,因此只要cLine被释放,d和e必然也作为垃圾被回收,即d和e在cLine的保留集内,因此cLine的深堆为16×2+16=48字节。
对于aLine和bLine对象,由于两者均持有对方的一个点,因此当aLine被回收时,公共点a在bLine中依然有引用存在,故不会被回收,点a不在aLine对象的保留集中,因此aLine的深堆大小为16+16=32字节。对象bLine与aLine完全一致。
在MAT中,无论是在柱状图还是对象列表中,选中对象并右击,在弹出的快捷菜单中都有ShowRetainedSet命令,它可用于显示指定类或者对象的保留集。图6.77和图6.78分别为在bLine对象上进行该操作,以及bLine对象的保留集。
MAT提供了一个称为支配树(DominatorTree)的对象图。支配树体现了对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。支配树是基于对象间的引用图所建立的,它具有以下基本性质:
·对象A的子树(所有被对象A支配的对象集合)表示对象A的保留集(retainedset)。
·如果对象A支配对象B,那么对象A的直接支配者也支配对象B。
·支配树的边与对象引用图的边不直接对应。
如图6.79所示,左图表示对象引用图,右图表示左图所对应的支配树。对象A和B由根对象直接支配,由于在到对象C的路径中可以经过A,也可以经过B,因此对象C的直接支配者也是根对象。对象F与对象D相互引用,因为到对象F的所有路径必然经过对象D,因此对象D是对象F的直接支配者。而到对象D的所有路径中,必然经过对象C,即使是从对象F到对象D的引用,从根节点出发,也是经过对象C的,所以对象D的直接支配者为对象C。
同理,对象E支配对象G。到达对象H的路径可以通过对象D,也可以通过对象E,因此对象D和E都不能支配对象H,而经过对象C既可以到达D也可以达到E,因此对象C为对象H的直接支配者。
在MAT中,单击工具栏上的对象支配树按钮,如图6.80所示,可以打开对象支配树视图。
图6.81显示了对象支配树视图的一部分。该截图显示部分main线程对象的直接支配对象,即main线程对象被回收后将被释放的所有对象的集合。
注意:在对象支配树中,某一个对象的子树表示在该对象被回收后也将被回收的对象的集合。
在Java系统中,作为垃圾回收的根节点可能是以下对象之一。
·系统类:被
bootstrap/systemClassLoader加载的类,例如在rt.jar包中的所有类。
·JNI局部变量:本地代码中的局部变量,例如用户自定义的JNI代码或者JVM内部代码。
·JNI全局变量:本地代码中的全局变量。
·线程:开始,并且没有停止的线程。
·在用同步锁:作为锁的对象。例如调用了wait()或者notify()方法的对象,或者调用了synchronized(Object)操作的对象。
·Java局部变量:如函数的输入参数及方法中的局部变量。
·本地栈:本地代码中的输入、输出参数,例如用户自定义的JNI代码或者JVM内部代码。
·Finalizer:在等待队列中将要被执行析构函数的对象。
·Unfinalized:拥有析构函数,但是没有被析构且不在析构队列中的对象。
·不可达对象:从任何一个根对象都无法到达的对象。但为了能够在MAT中分析,被MAT标志为根。
·未知对象:未知的根类型,用于处理一些特殊的堆格式。
通过MAT,可以列出所有的根对象,如图6.82所示。
MAT提供了自动检测内存泄漏,以及统计堆快照内对象分布情况的工具。图6.83展示了内存泄漏检测工具的使用方法。选择菜单中的LeakSuspects命令,MAT会自动生成一份报告。这份报告罗列了系统内可能存在内存泄漏的问题点。图6.84展示了报告中给出的一个问题点样例。
注意:仔细阅读MAT给出的内存泄漏报告,可以帮助开发人员更快地找到系统的潜在问题。
系统中占用内存最大的几个对象,往往是解决系统性能问题的关键所在。如果应用程序发生内存泄漏,那么泄漏的对象通常会在堆快照中占据很大的比重。因此,查看和分析堆快照中最大的对象具有较高的价值。
在MAT中,可以自动查找并显示消耗内存最多的几个对象。如图6.85所示,通过选择TopConsumers命令,可以打开消耗内存最多的对象的报告,其中主要以饼图和表格的形式来展示。
通过MAT,开发人员还可以很方便地查找某一个对象或者类的支配者(有关支配者的概念,可以参考6.7.3节“支配树”)。虽然在支配树页面中拥有完整的信息,但是通过MAT提供的支配者查找功能可以更方便地进行查找。图6.86显示了如何查找对象的支配者。
在选择ImmediateDominators命令后,会弹出一个参数对话框,用于设置查找参数,如图6.87所示。在参数对话框中,注意务必正确输入-skip参数,否则查询结果会忽略所有定义在-skip参数中的类和实例。
ImmediateDominators会输出选中对象的直接支配者(将-skip指定的对象排除在外)。
在堆快照中,还包括当前的线程信息,通过MAT可以查看这些信息。如图6.88所示,通过ThreadDetails、ThreadOverview和ThreadStacks这3个命令,可以查看线程详情。
图6.89所示为选择ThreadStacks命令后的输出结果,其中显示了当前堆快照中的所有线程及线程引用的对象。
MAT提供了一套对集合使用状态进行分析的工具,如图6.90所示。
使用这些工具,可以查看数组、集合的填充率;可以观察集合内的数据;也可以分析哈希表的冲突率。
注意:通过对集合使用情况进行分析,可以更好地了解系统的内存使用情况,查找浪费的内存空间。
选择CollectionFillRatio命令,可以展示给定集合的填充率。图6.91所示为该功能的输出结果,其中显示了填充率为0、20%以下、80%以下和100%以下的集合个数。
通过选择HashEntries命令,可以查看Hash表的内容。图6.92所示为该功能的一个输出示例,其中显示了选中的Hash表的内容。对于表中的Key和Value对象,通过右键快捷菜单,还可以进一步分析它们的引用情况和其他具体信息。
MAT是基于Eclipse开发平台的产品,因此它也具有很好的扩展性。开发者可以使用Eclipse对MAT进行扩展,从而实现符合开发人员需要的功能更加强劲的内存分析工具。通过扩展MAT,读者可以实现诸如自动对象查询、优化界面显示、报表增强等功能。本节将通过一个简单的MAT插件,介绍扩展MAT的基本步骤和方法。
注意:MAT是基于Eclipse的,因此对MAT进行二次开发与开发Eclipse插件非常类似。
在Java中,java.lang.String对象实现是基于内部的value字符数组、偏移量offset和字符串长度count来定义字符串String的真实取值的。如果内部数组value的实际长度很长,而字符串真实长度count的数值很小,则说明这个String的内存使用率不高,存在较为严重的内存浪费。
使用公式count/value.length可以计算当前String对象的内存使用率。在最优情况下,String对象的内存使用率是100%,即表示value数组中的所有字符都是当前字符串的内容。当使用类似String.subString()的函数生成新的字符串时,String对象通过调整offset和count,而非创建新的value数组来生成新的字符串,此时String对象的内存利用率就会下降。
本节中展示的插件将在显示String对象时,展示String对象的内存利用率,帮助开发者快速定位可以优化的字符串对象。
为扩展MAT,首先需要安装MAT程序及Eclipse开发工具。
(1)在Eclipse平台中添加MAT目标平台。在Eclipse中打开对话框:Windows|Preferences|Plug-inDevelopment|TargetPlatform。添加MAT平台,选择Add|Nothing|Next。在目标平台的Locations页面中,添加Installation,并指定MAT的安装路径,如图6.93所示。单击Finish按钮,并选择刚刚添加的MAT平台作为目标平台。图6.94所示为配置完成后的目标平台。
(2)创建一个插件工程。选择File|New|Other|Plug-inproject命令,假设工程名称是MATExtension,其他参数可以使用默认设置。创建完成后,在工程的Dependencies页面中添加org.eclipse.mat.api依赖,如图6.95所示。
(3)添加插件的扩展点。在本例中添加
org.eclipse.mat.api.nameResolver,如图6.96所示。在实际开发中,读者可以根据自己的需要,选择合适的扩展点增强MAT的功能。接着填写扩展点的具体信息,如实现扩展点接口的类名和包名,Eclipse会自动生成指定的类,如图6.97所示。
编辑生成的StringUsageDisplayer类,具体代码如下:
StringUsageDisplayer的功能是当MAT中显示String对象时,计算String对象的count值与value数组的长度比值。注释@Subject指定当前
IClassSpecificNameResolver只对java.lang.String对象有效。
(4)当完成开发后,还需要对插件进行打包。选择File|Export|Plug-inDevelopment|
Deployableplug-insandfragments命令,在打开的对话框中选中要打包的插件,并设置MAT的安装路径进行插件安装,如图6.98所示。
安装完成后,在MAT的plugins目录下就有了MATExtension插件的JAR包。
安装插件后的MAT,可以使用以下OQL查询取得所有内存利用率不是100%的String。
查询结果如图6.99所示,其中不仅显示了字符串的真实取值,也显示了当前字符串的内存使用率,可以帮助开发人员快速定位能够优化的字符串。
注意:通过对MAT的扩展,可以让MAT更贴近实际生产环境,使之更易于使用,提高了堆内存分析的效率。