3、MAT界面详解:Overview概览、Histogram直方图、Dominator Tree支配树、Path to GC Roots GC根路径
好,咱们直接进入正题。MAT这个工具,说白了就是帮你把堆内存dump文件“解剖”开来看。我第一次用的时候,面对一堆密密麻麻的数字,说实话有点懵。但摸清楚它四个核心视图之后,就豁然开朗了。
今天我就带你把这四个视图挨个过一遍。分别是:Overview、Histogram、Dominator Tree、Path to GC Roots。嗯,这四个是排查内存泄漏的“四件套”。
3.1 Overview概览:先看全局
打开MAT,加载完dump文件,第一个映入眼帘的就是Overview。我个人习惯,先扫一眼这个页面,心里就有个底。
它展示什么?最核心的是饼图——Biggest Objects by Retained Size。这个饼图告诉你,哪些对象占的内存最大。注意,是Retained Size,不是Shallow Size。Shallow Size是对象本身占的内存,Retained Size是“如果这个对象被回收,能释放多少内存”。后者才是我们真正关心的。
关键指标解读:
- Total heap size:堆总大小。如果这个值接近你设置的堆上限,那基本就是内存紧张了。
- Class loader:类加载器信息。我遇到过一个问题,就是因为自定义类加载器导致重复加载,内存里一堆重复的类。
- Actions:这里有几个快捷入口,比如Histogram、Dominator Tree。可以直接点进去。
我的小技巧:Overview页面右上角有个“Details”链接,点开能看到更详细的系统属性、线程信息。有时候排查线程泄漏,这里就能看出端倪。
3.2 Histogram直方图:按类统计
从Overview点进Histogram,或者直接点工具栏上的Histogram图标。这个视图,就是把所有对象按类名分组,统计每个类有多少个实例、占了多少内存。
你想想看,如果某个类的实例数量异常多,那大概率就是泄漏点。比如Activity、Fragment、Bitmap这些。
举个例子,我曾经排查一个图片列表滑动卡顿的问题。打开Histogram,按Retained Size排序,发现Bitmap对象占了将近200MB。再一看,每个Bitmap的像素尺寸都特别大。嗯,问题找到了——图片没做压缩。
Histogram的常用操作:
- 按包名过滤:在顶部输入框输入你的包名,比如
com.example,只看自己应用的对象。 - 右键 -> List objects:可以列出某个类的所有实例,看看具体是哪些对象。
- 右键 -> Merge Shortest Paths to GC Roots:这个操作可以直接跳到GC Root路径分析,非常实用。
注意:Histogram显示的是Shallow Size,不是Retained Size。所以排序时建议用“Retained Size”列排序,更准确。如果没看到Retained Size列,点一下列标题旁边的三角形就能切换。
3.3 Dominator Tree支配树:找“罪魁祸首”
Dominator Tree,我个人觉得是MAT里最强大的视图。它把对象之间的引用关系,转换成一棵树。树的每个节点,代表一个对象。父节点“支配”子节点——意思是,如果父节点被回收,子节点也会被回收。
为什么要看这个?因为很多时候,泄漏不是单个对象的问题,而是一整棵对象树都挂在某个地方。比如一个Activity泄漏,它可能持有一个很大的Bitmap,而Bitmap又持有像素数据。在Dominator Tree里,你一眼就能看到哪个对象是这棵树的“根”。
操作很简单:
- 点工具栏的“Dominator Tree”图标。
- 按“Retained Size”排序,从大到小看。
- 展开最大的节点,看看它下面挂了哪些子对象。
我记得有一次,一个同事说他的应用退出后内存一直不降。我打开Dominator Tree,发现一个Handler对象占了几十MB。展开一看,它持有一个Activity的引用,而Activity又持有一个ListView,ListView的Adapter里缓存了大量图片。说白了,就是Handler没在Activity销毁时清空消息队列,导致整个Activity树都泄漏了。
核心思路:在Dominator Tree里,找那些Retained Size特别大,但你又觉得“它不应该这么大”的对象。右键 -> Path to GC Roots -> exclude weak references,就能看到是谁在引用它。
3.4 Path to GC Roots GC根路径:追根溯源
这个功能,是前面所有操作的“终点站”。当你怀疑某个对象泄漏时,右键它,选择“Path to GC Roots”,然后选择“exclude weak references”。MAT就会画出一条从GC Root到这个对象的引用链。
GC Root是什么?简单说,就是JVM认为“活着”的起点。比如:
- 线程栈上的局部变量
- 静态变量
- JNI全局引用
- 系统类加载器
如果一条引用链上,所有引用都是强引用(Strong Reference),那这个对象就永远不会被回收。这就是泄漏的根本原因。
我曾经遇到一个经典案例:一个Fragment在onDestroyView之后,它的View对象仍然被一个静态的Drawable持有。通过Path to GC Roots,我看到引用链是:Static Drawable -> View -> Fragment。嗯,问题出在Drawable里注册了View的回调,但没在Fragment销毁时取消注册。
避坑指南:选择“exclude weak references”很重要。因为WeakReference、SoftReference这些,GC是可以回收的。如果选了“include all”,你会看到很多没用的路径,干扰判断。我曾经就因为这个,多花了半小时排查一个假阳性。
3.5 四个视图的配合使用
这四个视图不是孤立的。我一般的工作流是这样的:
- Overview:看一眼堆大小,确认是不是有问题。
- Histogram:按类统计,找到实例数异常多的类。
- Dominator Tree:从那个类里挑一个实例,看它支配了哪些对象,确认是不是泄漏树。
- Path to GC Roots:右键那个实例,找到引用链,定位到具体代码。
你想想看,如果直接跳到Path to GC Roots,你都不知道该查哪个对象。所以,按这个顺序来,效率最高。
总结一句话:Overview看全局,Histogram找异常,Dominator Tree定范围,Path to GC Roots抓真凶。这四个视图用熟了,内存泄漏在你面前就是透明的。
嗯,今天就先聊到这儿。下一节我会讲怎么用MAT的“Compare”功能,对比两个dump文件,找出内存增长点。那个功能在排查“缓慢泄漏”时特别好用。