分析和理解应用的内存使用情况是开发过程中一项不小的挑战。一个微小的逻辑错误可能会导致监听器没法被释放回收,最终导致可怕的内存溢出问题。甚至有时你已经释放了所有空对象,但是你的应用却多消耗了十倍甚至百倍的内存导致效率很低。
幸运的是,Eclipse Memory Analyzer(MAT)能给我提供应用的内存使用情况的详细信息帮助我们进行内存分析。这款工具不仅能有效的追踪内存泄漏,还能周期性的审查系统的状态。在本课程我将列出10条小技巧帮助你更高效的使用MAT。如果你是一名Java开发者,Eclipse Memory Analyzer Tool是你调试工具箱里必不可少的。
[ 你还在寻找更多工具吗? 查看Eclipse Tools页面. | 使用Yoxos.Create a free profile now让你更方便的管理你的Eclipse workspace. ]
可以使用Install New Software对话框或者通过EclipseMarketPlace来安装MAT。你也可以安装使用Yoxos将其囊括到你自己的Eclipse中。
在本例中,我们使用一个非常简单的方案,通过分配100,000监听器,并将它们存储到4个列表中。在未将列表清空回收的情况下让应用休眠。
1.获取内存快照(Heap Dump)
你可以通过下面的几种方式使用MAT:
1.配置一款应用,当其发生内存溢出错误的时候将其内存镜像导出,
2.将MAT连接到一个已存在的Java进程,或者
3.手动获取heap dump并加载到MAT中。
无论哪种情况,你都需要记住这只是内存在某一时间节点的快照。MAT不能告诉你为什么一个对象会被创建,也不能显示那些已经被回收掉的对象。但是,如果你使用MAT结合其他的调试工具和调试技术,通常会非常快的解决内存泄漏。
你可以通过添加下面的vm argument,配置你的应用当其抛出OutOfMemory错误的时候导出heap dump:
-XX:+HeapDumpOnOutOfMemoryError
另外,你也可以使用jstack从正在运行的Java进程中获取Heap dump.
jmap -dump:file=heap.bin
最后,你还可以使用MAT的Acquire Heap Dump动作选中你本地机器上已经存在的Java进程。
当你第一次加载Heap dump的时候,MAT需要花几分钟时间来给Heap dump编辑索引。其结果会保留所以后续的再次加载会很快。
2.理解Histogram
当你第一次获取heap dump的时候,MAT会给你展示应用的内存使用情况的overview。
中间的饼状图给你展示的是retained size最大的对象。也就是说,如果我们能释放一个特殊的java.lang.Thread对象,就能保留11.2Mb的内存,超过你当前应用使用内存的90%。有趣的是,java.lang.Thread并不像是问题的症结所在。为了更好的理解到系统当前存在的对象,我们可以使用Histogram。
Histogram可以展示某个特定类的对象个数和每个对象使用的内存。当然char[],String和Object[]都不太会导致内存问题。为了更好的组织这个视图,你可以通过classloader或者package来分组。这会让你更好的专注在你自己的对象上。
Histogram 也能使用正则表达式来过滤。例如,我们可以只展示那些匹配com.example.mat.*的类。
通过这个视图我们可以看见在系统中存在100,000个Listener的对象。我们也可以看见每一个对象正在占用的内存数量。这里有两个数值,Shallow Heap和Retained Heap。Shallow heap是一个对象消费的内存数量。每个对象的引用需要32(或者64 bits,基于你的CPU架构)。基本数据类型例如整形和长整形需要4或者8 bytes以及其他的。其实更有用的参数是Retained Heap.
3.理解Retained Heap
Retained Heap显示的是那些当垃圾回收时候会清理的所有对象的Shallow Heap的总和。举例说明,如果一个ArrayList包含100,000成员项,每个成员需要16 bytes,当移除这个ArrayList的时候会释放16x100,000+X(bytes),X是ArrayList的shallow size。(注:这是假设这些对象只被这个ArrayList引用,没有其他地方引用)。
Retained heap是Retained set(保留集)里面所有对象大小的求和计算结果。Retained set of X指的是这样的对象集合: X 对象被 GC 回收后,所有能被回收的对象集合。
Retained heap有两种不同的计算方式, 使用quick approximation或者precise retained size.
通过计算Retained Heap我们可以看见com.example.mat.Controller持有了大部分的内存,尽管他自身只占用了24 bytes。所以通过找到方法释放Controller,我们就能毫无疑问的控制好内存问题。
4. Dominator Tree(支配树)
查看Dominator tree是理解Retained heap的关键。Dominator tree是由你系统中的复杂的Object graph(对象引用图)生成的树状图。Dominator tree可以让你分别出最大内存图表。如果所有指向对象Y的路径都经过对象X,则认为对象X支配对象Y。通过查看本例的Dominator tree,我们开始明白到底是哪些内存块发生了泄露。
通过查看dominator tree,我们可以轻易的了解到并不是java.lang.Thread导致的问题,反而是Controller和Allocator持有内存。Controller保留了全部100,000个Listeners对象。我们可以通过释放这些对象,或释放他们所包含的lists来改善内存情况。下面列出几条dominator tree的属性:
● 对象X的子树中的所有对象(本例中的com.example.mat.Controller)被称作对象A的Retained set(保留集)。
● 如果对象X是对象Y的直接支配者(Controller就是Allocator的直接支配者),那么X的直接支配者(本例中的java.lang.Thread)也只配Y对象。
● 支配树中节点的父子关系跟对象引用图中的不直接对应。
通过Histogram你也可以选择某个类,然后找到所有支配该类的实例的对象。
5. 探索Paths to the GC Roots
有时候有一些你确信已经处理了的大的对象集合。通过查找支配者可能会有用,但是通常我们希望能得到这个对象节点到GC根节点的路径。例如,如果我现在释放了Controller对象,会理所当然的以为已经解决内存问题,不幸的是这并没有用。如果现在选中一个Listener的对象,然后查看他到GC根节点的路径。我们可以看见Controller类(注:是类,而不是对象)引用到了一个Listener队列。这是因为这些队列当中有一个被声明成静态队列。
你也可以查看到这个对象所有被引用到的地方和这个对象持有的引用。当你想要在对象引用图中查看某个特定对象的所有引用关系的时候,这是非常有用的。
6. Inspector
Inspector展示的是当前选中类或对象的详细信息。在本例中我们可以看见选中的ArrayList包含100,000元素和一个指向地址为0x7f354ea68的对象数组的引用。
Inspector和Snapshot linked会给你提供一些选中项的重要统计数据。
7. Common Memory Anti-Patterns
MAT使用反模式提供了公用存储器的详细报告。.能用其来搞明白哪里的发生了内存泄漏,或通过它找到一些简单的清理手段来优化性能。
Heap Dump Overview展示了Heap Dump的详细信息和一些常用工具的链接(比如Histogram)。信息主要有系统中正在运行的线程、对象的总数、堆的大小等。
Leak Suspects报告显示了MAT发现的可能导致内存泄漏的地方,和用于分析这些发现的工具和图表的链接。
另一个使用到反模式的情况是,当系统有大量的集合,但是每个集合只有少量元素的时候。例如,如果每一个监听器都对应一组通知者(需要某些事件来触发的列表项),但是这些通知者只是偶尔触发,我们就应该制止这种浪费内存的行为。Java Collections工具可以帮你处理这类问题。
通过Collection -> Fill Ratio Report我们可以看见100,000个队列是空的。如果我们能够用一种便捷的方式来分配这些内存(当我们需要的时候),我们可以节约大概8Mb内存。
我们也可以通过分析集合来查看array fill ratios、collection size statistics和map collision ratios。
8. Java工具
MAT量身定制了许多内置的工具用来生成Java运行环境细节的相关报表。For example, thereport will show details about all the treads in the system.例如,Threads and Stack可以展示系统中所有线程的细节。你可以看见每个栈中当前存在的本地变量
你可以通过特定的模板来检索所有匹配的字符串:
甚至可以检索那些包含了浪费内存的字符数组的字符串(这种情况经常是因为反复是用substring方法导致的)。
9. Object Query Language
综合以上所说,Eclipse Memory Analyzer提供了很多用来追踪内存泄漏和内存过量使用的工具。大多数的内存问题可以通过上面的工具定位到,但是Heap Dump包含了更多的信息。Object Query Language (OQL)让你可以基于Heap Dump创建你自己的报表。
OQL是一种类似于SQL的语言。只需要将类当成表,对象看做行,字段看做列。例如,想要查询com.example.mat.Listener的所有对象,只需要写:
select * from com.example.mat.Listener
表的列可以通过不同的字段来设置,例如:
SELECT toString(l.message), l.message.count FROM com.example.mat.Listener l
And finally, the WHERE clause can be used to specify particular criteria, such as all the Strings in the system which are not of the format “message:.*”最后WHRER子句可以用来筛选特定的条件,例如可以通过下列语句找出系统中所有不匹配"message:.*"的字符串:
SELECT toString(s), s.count FROM java.lang.String s WHERE (toString(s) NOT LIKE "message.*")
10.导出结果
MAT是一款用来导出应用内存状态相关报告的利器。Heap Dump包含了有关你系统的非常有价值的信息。并且MAT提供了相关的工具来接入这些数据。然而,就像很多开源工具一样,如果你对于某些失误不太敏感,或者你运气不好。使用MAT可以将结果导出成包括HTML,CSV甚至纯文本格式。你可以使用电子表格程序(或者你自己的工具)来继续进行分析。
MAT是一款强大的工具,一款Java开发者应该熟知的工具。追踪内存泄漏和其他的一些内存问题对开发者来说是常见的难点,可喜的是有MAT可以迅速的帮你找到与你内存问题的源头所在。
英文原文:10 Tips for using the Eclipse Memory Analyzer « EclipseSource Blog
参考:Android 内存剖析 - 发现潜在问题 - ImportNew