使用MAT的10个小技巧
分析和理解应用的内存使用极具挑战。稍微一点逻辑错误就会导致监听器得不到处理,最终导致一个内存溢出错误。即使你的应用正确了所有未使用的对象,也可能需要比必需大10到100倍的内存。
很幸运,MAT可以提供详细的应用内存使用情况。这款工具在内存泄漏追踪以及系统状态定期巡检方面都可以给你强有力的帮助。本文给出10个更有效的使用MAT的小技巧。如果你是一个Java工程师,MAT应该是你日常debug 的必备工具。
MAT 可以以eclipse插件的方式安装,也可以独立安装。
在这个例子中,分配100000个监听器,放在4个不同的list中。应用程序不会清理这些list.
1、获取heap dump 文件
好多途径获取:
- jvm配置
- 使用MAT直连Java进程
- 手动获取一个heap dump然后加载到MAT 中。
切记,上述都是获取某一时刻的内存映像。MAT 不能告诉你对象为什么创建,也不能展示那些已经被回收的对象。如果你结合这篇文章https://eclipsesource.com/blogs/2013/01/08/effective-java-debugging-with-eclipse/ ,你将会迅速找到内存泄露问题。
配置 -XX:+HeapDumpOnOutOfMemoryError 这个jvm参数,可以在内存溢出的时候输出文件。
或者你可以使用jmap获取某一时刻的内存映像。 jmap -dump:file=heap.bin 。
然后使用MAT 分析。在第一次加载heap dump文件时,MAT会建立索引。这会消耗几分钟,但是结果会保留,后续的分析将会非常快。
2、理解直方图(Histogram)
第一次打开heap dump,会看到一个应用服务内存占用的overview图。中间的饼状图展示了 retaind size 较大的对象。图中表明,如果处理了java.lang.Thread 对象,将会节约11.2M,接近90%的内存。但是比较有意思的是java.lang.Thread 看上去并不想问题所在。为了更好地查看当前存在的对象,请使用直方图(Histogram)。
直方图中展示了一个特定类的实例数目以及每一个实例使用的内存。当然, char[], String 以及Object[] 并不像是问题所在。为了更好地归类这些现象,可以按照类加载器或者按照包进行划分。这可以使你聚焦在有自己应用服务的对象上。
直方图可以使用正则表达式进行过滤。比如我们可以只匹配模式符合 “com.example.mat.*”的类。
通过上图,可以看出有100000个listner 对象。也可以看到每个对象占用的内存。大小使用了shallow heap 跟 retained heap 两种对象。具体的含义可以参考 《入门前必读文章》。 shallow heap 对象自身占用的内存你大小,每个对象占用32 或者 64位主要依赖于数据类型。比如整型跟long分别占用4byte 跟 8byte. Retained Heap 比较有用,我们着重来分析。
3、理解Retained Heap
retained heap 展示了如果一个对象被GC,总共会有多少空间会被释放出来。比如 一个arraylist 包含100,000 个值,每个占用16byte, 如果删除这个Arraylist 将会释放 16 * 100,000 + X, X是ArrayList自身的大小。
retained heap 会把一个对象的retained set 中所有的对象的大小相加。 对象的retained set 是指如果对象被回收,则set集合中的对象将都会被回收。可以精确计算,也可以快速近似计算。
对象可能自身占用比较小,但是其retained size也许会很大。
4、支配树(Dominator tree)
通过看支配树才能更好地理解retained heap. 支配树是应用系统复杂对象的关系图树。支配树可以确定大对象的内存图。如果Y的path to GC ROOTS 经过了X,则说X支配了Y。通过支配树可以看出哪个地方引起了内存泄露。
通过支配树可以看出问题不是出在java.lang.Thread,而是在controller 跟allocator. 100000个对象都在Controller的retained set中。释放这些对象,问题将会得到解决。有几个比较有用的支配树属性:
- 所有属于X子树中的对象都位于X的retained set中。
- 如果X支配Y,那么X的支配者也是Y的支配者。
- 树中的父子关系并不一定跟对象图中的关系一致。
在直方图中,可以选择一个特定的类,然后找出这个类的实例。
5、GC Roots 引用路径 (Paths to the GC Roots)
有时候,你会有一堆需要处理的对象。找到支配关系会有所帮助,但是更多时候你需要找到这个对象到GC root 的引用路径。比如,这个例子中,如果处理了Controller ,内存问题会不会解决呢?答案是不会的。如果选择一个Listener的实例,会发现Controller 类 被一堆Listener list引用了。
也可以看一个对象的incoming 和 outgoing 引用。如果想看对象图中一个特定对象的引用,这非常有帮助。
6、明细 (Inspector)
Inspector 提供了一个具体类或者对象的详情。在这个例子中可以看到Arraylist 包含10,000个元素,并且引用了位于0x7f3543占用0x7f354ea68内存的数组对象。
使用inspector跟内存映像链接将会展示很重要的详细统计信息。
7、常用内存反面模式 (Common Memory Anti-Patterns)
MAT 为内存分析提供了相关的一些反面模式报告。这些能够看到内存泄漏时系统的整体状况,或者可以通过回收一些低效的内存占用来提升性能。
heap dump overview 文件展示了heap dump 文件的详细信息,提供了很多工具。比如像运行时的线程数,系统中的对象总个数,堆的大小等。
内存泄漏可疑报告展示内存可能存在的泄漏点,并且提供了工具和图标去分析这些发现。
另外一个通用反面模式是数量巨大但是每一个元素占用很小的collections的使用。比如例子中 每个liseners都会分配一个有关notifiers的数组,但是这些notifierds 偶尔才使用。可用通过结束这些来节约空间。 Java Collections 工具可以帮助分析这些问题。
通过使用Collection -> Fill Ratio Report分析发现,10,000 个arraylist 是空的。如果使用懒加载的方式,将会节省8M.
也可以使用Collection 分析去查看 array fill ratios, collection size statistics 以及 map collision ratios
8、Java 工具
MAT有很多内置的工具去生成报告,用来分析Java运行时的细节。比如线程和堆栈报告 用来展示系统中的线程。可以看到每个线程中存活着哪些本地变量。
也可以在系统中找到匹配某个模式的字符串。
甚至可以找到系统中位于char数组中浪费空间的字符串。
9、OQL (Object query language)
MAT 提供了无数的工具用来分析内存泄露以及大内存使用。heap dump 文件包含了很多信息, 很多内存问题可以使用技术描述。 你可以利用OQL分析heap dump ,产生报告。
OQL 类sql语言。可以把类比作table, 对象比作 row, 属性比作columns. 比如,展示com.example.mat.Listener 的对象,可以这样写:
select * from com.example.mat.Listener
columns 可以表示不同的属性,比如
SELECT toString(l.message), l.message.count FROM com.example.mat.Listener l
最后,WHERE 查询可以表示特定的条件。比如系统中不匹配message 的字符串:
SELECT toString(s), s.count FROM java.lang.String s WHERE (toString(s) NOT LIKE "message.*")
10、生成你的报告
支持html csv txt 格式
参考文献
eclipse 中非常不错的debug工具: https://eclipsesource.com/blogs/2013/01/08/effective-java-debugging-with-eclipse/
名词解释
- 反面模式: 在软件工程中,一个 反面模式(anti-pattern或antipattern)指的是在实践中明显出现但又低效或是有待优化的设计模式,是用来解决问题的带有共同性的不良方法。 https://zh.wikipedia.org/zh-hans/反面模式