赞
踩
目录
Histogram和Dominator Tree,大多数时候用到的也是这两个点:
Merge Shortest Paths to GC Roots
Merge Shortest Path To GC Roots
MAT (Memory Analyzer Tool)
导出方法:
AndroidStuido
命令
DDMS
2. hprof 文件转换,转换为MAT可以识别的文件
hprof-conv 命令 :
hprof-conv -z srcFile dstFile
如下;
hprof-conv -z a.hprof b.hprof
① -z 命令选项 : 表示排除非 APP 内存的堆 , 如 Zygote 内存等 ;
② srcFile 参数 : -z 后第一个参数是 源文件 , 即从 Android Studio 中保存的内存快照文件 , memory-20200625T145636.hprof ;
③ dstFile 参数 : 后面的第二个参数是目标文件 , 即将转换后的结果保存到该文件中
3.使用MAT打开hprof文件
1、Histogram:是针对对象的数量,可以这么理解,一个类可以创建多个对象,这里查看的就是一个类总共创建了多少个对象;
2、Dominator Tree: 支配树,是针对的对象引用关系,以及该类所有实例对象所占用内存的百分比;
可以直观地反映一个对象的retained heap,这里我们首先要了解两个概念,shallow heap和retained heap:
retained heap值的计算方式是将Retained set中的所有对象大小叠加。或者说,由于X被释放,导致其它所有被释放对象(包括被递归释放的)所占的heap大小。
Retained Set 当X被回收时那些将被GC回收的对象集合。
比如: 一个ArrayList持有100,000个对象,每一个占用16 bytes,移除这些ArrayList可以释放16 x 100,000 + X,X代表ArrayList的shallow大小。相对于shallow heap,Retained Heap可以更精确的反映一个对象实际占用的大小(因为如果该对象释放,retained heap都可以被释放)
选择exclude all phantom/weak/soft etc.references, 意思是查看排除虚引用/弱引用/软引用等的引用链
List objects with (以Dominator Tree的方式查看)
如下图
检测每一个HashMap或者HashTable实例并按照碰撞率排序 碰撞率 = 碰撞的实体/Hash表中所有实体
当Hash集合中过多的对象返回相同Hash值的时候,会严重影响性能(Hash算法原理自行搜索),这里来查找导致Hash集合的碰撞率较高的罪魁祸首
HashEntries
通过查看key value
- select * from java.util.ArrayList where size=0 and modCount=0 类似的
- select * from java.util.HashMap where size=0 and modCount=0
- select * from java.util.Hashtable where count=0 and modCount=0
Overview 中的饼图中 , 列出了占用最大内存的对象 ;
对象的引用与被引用 : 右键点击某对象 , 选择 List objects 选项 ;
① 查看该对象引用了哪些对象 : 选择 with incoming reference 选项 ;
② 查看该对象被哪些对象引用 : 选择 with outgoing reference 选项 ;
查看对象到 GC Roots 的最短距离 : 在右键菜单中选择 " Merge Shortest Paths to GC Roots " , 这里就可以看到为什么对象可达性分析时 , 某些对象应该释放 , 却仍然存在与 GC Root 对象之间的引用链 ;
GC Roots 与 GC 垃圾回收
存在与 GC Roots 引用链导致内存泄漏 : 该对象可能与 GC Root 对象不是直接引用 , 而是由其它对象间接引用 , 导致存在这么一条引用链 ;
具体的 GC 回收原理在 【Android 内存优化】Java 内存模型 ( Java 虚拟机内存模型 | 线程私有区 | 共享数据区 | 内存回收算法 | 引用计数 | 可达性分析 ) 博客中的可发行分析章节 , 有详细的介绍 , 如果 GC Root 对象与某个对象之间有引用链 , 那么该对象无法被 GC 回收 ;
- with all reference : 列出所有的引用 ;
- exclude weak reference : 排除弱引用 ;
- exclude soft reference : 排除软引用 ;
- exclude phantom reference : 排除虚引用 ;
- exclude weak/soft reference : 排除弱引用和软引用 ;
- exclude all phantom/weak/soft etc. reference : 排除虚引用 , 弱引用 , 软引用 ; 只查看强引用 ;
查看强引用引用链 : 这里选择 exclude all phantom/weak/soft etc. reference 选项 , 可查看到 GC Roots 的强引用引用链
从图看出该 AppCompatTextView 类有三个对象的引用链 :
这样就找到了是哪个类引用了我们要查找的目标对象 , 然后在对应代码中查找为什么没有释放这个类 。
这里以byte[]为例,这里的Objects是1670,就是说内存中byte[]对象的数量是1670,Shallow Heap的大小13568768,这个大小代表的是这1670个byte[]对象所占用的内存,关于Shallow Heap和Retained Heap的意思其实就是上面提到的Shallow Size和Retained Size,这样看着就和Android Profiler是一样的了,接下来就来看看MAT是如何去对比两份hprof文件的,这里是在Histogram界面,然后在下面的Navigation History选中histogram点击右键,在点击Add to Compare Basket,如下图:
这样操作完后在点击上图红框标示的地方,就可以得到对比的结果,如下图:
这个图就是hprof的对比结果,可以看到byte[]类的对象由1670增加到了1687个,所占用的内存由13568768增加到了54004896个byte,如果觉得这样的对比结果不够明显,那么还可以点击上面的红框部分进行计算对比,比如:
对于这样的一份结果,我们应该主要去分析内存泄露比较大的地方,这里占用内存大的地方是byte[],那我们就可以右键点击做如下操作:
这样就可以得到GC Root的引用,如下图:
但是结果并没有找到GC Root,所以分析到这就跟不下去了,不过到这我们可以结合下Android Profiler,在Android Profiler中是可以找到GC Root,并且可以知道这份byte[]是一份什么样的数据,比如是图片,那么就可以知道这张图片是什么样的,这里针对的是byte[]数据,如果是是我们自己定义的类对象,一般都是可以找到GC Root的,如果是在不行,那就再回过头来结合Android Profiler分析了。
总结:
1、对于由于静态内部类、静态引用等引起的内存泄露,这部分内存泄露是比较好分析,直接使用LeakCanary就可以,可以帮助解决绝大部分的内存泄露。
2、如果还需要细致的内存分析,首先使用的应该是Android studio自带的Android Profiler,这个自带的工具也是很强大的,使用的好基本上是可以帮助我们解决内存泄露的。
3、对于使用Android Profiler还是分析不出来,那就在结合MAT工具了,内存泄露基本就没什么问题了。
上面主要介绍的是MAT的两个结果分析界面,其实,配合不同的查询选项,可以将它们的威力最大化。MAT中常用的查询选项集合都在顶部以图标的方式进行了显示
上图从左到右依次为:
等等
在MAT中支持对象查询语句,这是一个类似于SQL语句的查询语言,能够用来查询当前内存中满足指定条件的所有的对象。OQL将内存中的每一个不同的类当做一个表,类的对象则是这表中的一条记录,每个对象的成员变量则对应着表中的一列。
OQL的基本语法和SQL相似,语句的基本组成如下
SELECT * FROM [ INSTANCEOF ] <class name="name"> [ WHERE <filter-expression> ] </filter-expression></class>
所以,当你输入
select * from instanceof android.app.Activity
的时候,就会将当前内存中所有Activity及其子类都显示出来。但是OQL也有一些特别的用法。下面简单介绍一下OQL各部分的基本组成(更详细的可以参看这里)。
首先是OQL的from部分,指明了需要查询的对象,可以是如下的这些类型:
一个类的完整名称,例如
select * from com.example.leakdemo.LisenerLeakedActivity
SELECT * FROM "java\.lang\..*"
select * from java.io.File[]
select * from 0x2b7468c8, 0x2b7468dd
select * from 66888
甚至可以是另外一个OQL的查询结果,以实现级联查询
SELECT * FROM (SELECT * FROM java.lang.Class c WHERE c implements org.eclipse.mat.snapshot.model.IClass )
from部分还可以用如下两个关键词进行修饰
SELECT * FROM INSTANCEOF android.app.Activity
SELECT * FROM OBJECTS com.example.leakdemo.LisenerLeakedActivity
返回的就是LisenerLeakedActivity对应的类的信息,而不是内存中存在的ListenerLeakedActivity对象。
from部分还能为查询指定别名,以便提高可读性。例如
SELECT result.mType FROM OBJECTS com.example.leakdemo.LisenerLeakedActivity result
这里的result即是我们指定的别名,在我们不指定的情况下OQL默认别名为空,如下语句和上面语句效果是相同的
SELECT .mType FROM OBJECTS com.example.leakdemo.LisenerLeakedActivity
这部分和WHERE部分是最灵活也是最复杂的部分,但是大部分情况下我认为在这里使用“*”最好,因为一般这样我们的结果里面的信息是最全。不过如果你不想被结果中的其他信息干扰,可以自定义这部分的内容来实现。
OQL可以通过自定义select来实现对内存中对象或者类的各种属性及方法结果等进行查询,这主要通过以下三种表达方式来实现。
[ <alias>. ] <field> . <field>. <field>
其中alias代表别名,是在from部分中定义的,可以省略;field就是对象中定义的属性。这种方式能够访问到查询对象的属性值,例如:
SELECT result.mType FROM OBJECTS com.example.leakdemo.LisenerLeakedActivity result
能够展现出LisenerLeakedActivity.mType组成的查询结果。
查询访问Java Bean及该对象在MAT中的属性,需要的表达式为:
[ <alias>. ] @<attribute>
通过这种方式能够查询到的内容比较多,我们可以通过使用MAT中对OQL的自动补全功能(点这里查看)来选择需要查询的内容,例如:
SELECT result.@clazz AS "class", result.@displayName AS "display name" FROM com.example.leakdemo.LisenerLeakedActivity result
就可以得到对象对应的class信息和对象在heap dump中的显示名称。
[ <alias> . ] @<method>( [ <expression>, <expression> ] ) ...
注意method后面的括号是必不可少的,否则OQL会认为这是一个属性名称。”@”符号在新版本的OQL已经可以不需要了,例如:
SELECT result.@clazz.hasSuperClass(), result.getField("mType") FROM com.example.leakdemo.LisenerLeakedActivity result
可以得到当前对象是否有基类,以及对象中的mType属性的值是多少。
不仅如此,select部分还支持很多有用的内建函数,这里就不一一介绍了,大家可以戳这里查看。
关于select部分的更多信息还可以到这里具体查看。
灵活设置where参数可以为我们排除很大一部分的干扰信息,这部分对于属性和方法的支持和select部分相同,支持的运算符号和关键字有:
<=, >=, >, <, [ NOT ] LIKE, [ NOT ] IN, IMPLEMENTS, =, !=, AND, OR
相信大家都清楚这些关键值的意义,举一个简单例子
SELECT * FROM com.example.leakdemo.LisenerLeakedActivity result where result.mType = 1
上面那个查询语句可以查询到所有LisenerLeakedActivity.mType为1的对象。关于where部分的相信介绍可以参考这里。
OQL可以用来排查缓慢内存泄漏的情况,这种形式的内存泄漏只有程序长时间运行才会造成OOM,在排查阶段由于泄漏不严重而不能够在Dominator Tree或者Histogram中明显表现出来,但是如果你清楚在当前阶段程序中的一些重要对象的大概数量的话,则可以通过OQL来查询验证。这种方式对于在Android中排查Activity泄漏十分有用,例如有下面的代码:
- public class LisenerLeakedActivity extends Activity {
- private static final String TYPE_KEY = "type_key";
- private DemoListener mListener;
- private int mType = -1;
- public static void startListenerLeakedActivity(Context context, int actiivtyType) {
- Intent intent = new Intent(context, LisenerLeakedActivity.class);
- intent.putExtra(TYPE_KEY, actiivtyType);
- context.startActivity(intent);
- }
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mType = getIntent().getIntExtra(TYPE_KEY, -1);
- mListener = new InnerListener();
- DemoListenerManager.getInstance().registerListener(mListener);
- }
- public int getActivityType() {
- return mType;
- }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- // 故意制造泄漏
- // if (null != mListener) {
- // DemoListenerManager.getInstance().unregisterListener(mListener);
- // }
- }
- private class InnerListener implements DemoListener {
- @Override
- public void nothingToHandle() {
- // nothing to do
- }
- }
- }

上面Activity会在onCreate中向DemoListenerManager注册一个Listener,而这个Listener是Activity的内部类,因此它持有这个Activity的引用,如果在Activity的onDestroy阶段没有及时的反注册,那么这个Activity就泄漏了。现在随着多次启动和关闭这个Activity,我们可以发现gc后的内存总是不断增长,这是一个内存泄漏的特征,于是我们dump出当前内存,但是在其中的Dorminator Tree和Histogram中都不能很明确地得出问题所在,于是我们只能按照经验使用OQL来排查。假设你对Activity的泄漏有所怀疑,通过简单输入:
select * from instanceof android.app.Activity
可以得到如下结果:
从结果当中可以看到,当前内存中存在多个ListenerLeakActivity,而按照我们的原则,离开一个Activity就应该及时销毁和释放改Activity的资源,因此这就说明了Activity的泄漏是造成泄漏的原因之一。
这部分的功能其实和在显示列表中点击右键得到的菜单相同,如下所示:
如同:
下面挑选比较重要的几个选项来说明。
有两个选项:with incoming refrences和with outcoming refreences,分别会罗列出选定对象在引用树中的父节点和子节点,可以作为为什么当前对象还保留在内存中的基本依据。在平时的排查过程中,此功能还是比较常用,因为可以顺着罗列出的引用路径一步步分析内存泄漏的具体原因,例如,如下是上面Demo中针对ListenerLeakedActivity的一个with incoming reference的结果,从这个结果中可以逐步展开,最后得到是因为DemoListenerManager的单例仍然持有ListenerLeakedActivity的InnerListener造成泄漏的结论。
注意,如果你选择with incoming references得到的每个结果展开后仍然是针对选择展开的内容的with incoming references,对于with outcoming references也一样。因此,如果两个对象存在循环引用,你需要仔细分析,排除循环引用的路径。例如上图中就不能再展开引用源:ListenerLeakedActivity的mListener进行分析,因为ListenerLeakedActivity本身就和InnerListner存在相互引用关系,展开就又得到了ListenerLeakedActivity的incoming references结果,和一级列表结果是一样的,一直这样展开下去就真是子子孙孙无穷匮也了。
和List Objects相似,也可以罗列出当前选中对象在引用树种的上下级关系,只是这个选项会将结果中类型相同的对象进行归类,比如结果中有多个String类型的对象,List Objects会将它们分别显示出来,而Show objects by class会将它们统一归类为java.lang.String。虽然相对来说结果显示简洁了,但是我个人在平时分析的过程中一般较少使用到这个功能。
这是快速分析的一个常用功能,它能够从当前内存映像中找到一条指定对象所在的到GC Root的最短路径。这个功能还附带了其他几个选项,这几个选项分别指明了计算最短路径的时候是否是需要排除弱引用、软引用及虚引用等。 一般来说这三种类型的引用都不会是造成内存泄漏的原因,因为JVM迟早是会回收只存在这三种引用的资源的,所以在dump内存映像之前我们都会手动触发一次gc,同时在找最短引用路径的时候也会选择上exclude all phantom/weak/soft etc. references选项,排除来自这三种引用的干扰。如下是针对ListenerLeakedActivity的一次Merge Shortest Path To GC Roots的结果,从图中可以明显地看到一条从sInstance->mListener->array->ListenerLeakedActivity的引用路径,这个结果已经足以定位Demo中内存泄漏的原因了。
这个选项并不是很常用,这里只简单介绍一下其中的Class Loader Explorer子选项,它可以用来查看一些和选定对象的class loader相关的特性,包括这个class loader的名称、继承关系和它所加载的所有类型,如果你的程序当中应用到了自定义class loader或者osgi相关的特性,这个选项会为你分析问题提供一定程度的帮助。如下是我们通过Demo中的ListenerLeakedActivity的Class Loader Explorer可以看到如下结果。结果表明ListenerLeakedActivity是由PathClassLoader加载的,同时罗列了由这个class loader加载的其他8个类型以及这些类型目前在内存中的实例数量。
这块的功能平时确实少有用到,对其中的功能应用不太了解,大家有兴趣可以参考这里。也欢迎大家帮助我补充一下这块的功能应用。
这是一个非常有用的功能,它的作用就是找出选择对象在Dominator Tree中的父节点。根据我们之前对Dominator Tree的阐述,就是找到一个对此对象存在于内存中负直接责任的对象,这个方法一般和List Objects或者Merge Shortest Path To GC Roots相互结合使用,共同定位或相互佐证。因为去掉了一个节点在引用树中的一个父节点并不一定能保证这个对象会被回收,但是如果去掉了它的Immediate Dominator,那这个对象一定会被回收掉;但同时,有时候一个对象的Immidate Dominator计算出来是GC Roots,因为它处在引用树中的路径没有一个非GC Roots的共同点,这个时候我们直接用Immidiate Dominators就不太好分析,而需要借助List Objects或者Merge Shortest Path To GC Roots的功能。因此,这几个方法需要相互配合使用。
这个功能简单来说就是显示选定对象对应在Dominator Tree中的子节点集合,所有这些子节点所占空间大小对应于它的retained heap。可以用来判断当一个对象被回收的话有多少其他类型的对象会被同时回收掉,例如,我们上面的Demo中,因为InnerListener同时被ListenerLeakedActivity和DemoListenerManager所持有,因此它不在这两个对象任何一个的retained set中,而是在这两个对象的公共节点GC Roots的retained set中,但是如果我们没有向DemoListenerManager中注册过InnerListener,那么它会出现在ListenerLeakedActivity的retained set中。
这个选项支持将列表显示的结果按照继承关系、包名或者class loader进行分组,默认情况下我们结果显示是不分组的,例如Histogram和Dominator Tree的查询结果只是将对应引用树和Dominator Tree中的各个节点罗列出来,这样就包含了很多系统中的其他我们不关注的信息,使用分组的方法就可以方便聚焦到我们关注的内容之中。例如,下面分别是我们对Demo的Histogram视图结果进行按照包名和继承关系进行分组的结果显示。从中看到按照包名排序的方式方便我们着重关注那些我们自己实现的类在内存中的情况,因为毕竟一般情况下出现泄漏问题的是我们自己的代码;而按照继承关系的方式进行分类我们可以直接看到当前内存中到底有多少个Activity的子类及其对象存在,方便我们重点排查某些类。
在我们一开始选择Histogram或者Dominator Tree视图查询结果的时候,MAT并没有立即开始为我们计算对应各个节点的Rtained Heap大小,此时我们可以用这个功能来进行计算,以方便我们进行泄漏的判断。这个选项又包含两个算法,一个是quick approx的方式,即快速估算,这个算法特点是速度快,能够快速计算出罗列各项Retained Heap所占的最小内存,所以你用这个方式计算后看到的Rtained Heap都是带有”>=”标志的;如果你想精确地了解各项对应Retained Heap的大小,可以选择使用Calculate Precise Retained Size,但是根据当前内存映像大小和复杂度的不同,计算过程耗时也不相同,但一般都比第一种方式慢得多。
关于Android内存泄漏检测的一个神器LeakCanary,大家可以看另外一篇博客
Example
在android中ListView对象是可以不断对其子view进行服用来达到提高内存使用效率的目的,ListView的这一特性依赖于List Adapter的getView的实现,我们通过如下代码来故意使ListView无法复用其子view,从而模拟对象膨胀的情况,代码如下:
- public class LeakedListviewExample extends Activity {
- private ListView mLeakedListview;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.leaked_listview);
- mLeakedListview = (ListView) findViewById(R.id.leaked_listview);
- mLeakedListview.setAdapter(new LeakedAdapter());
- }
- private class LeakedAdapter extends BaseAdapter {
- @Override
- public int getCount() {
- return 1000;
- }
- @Override
- public Object getItem(int position) {
- return String.valueOf(position);
- }
- @Override
- public long getItemId(int position) {
- return position;
- }
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- // 不复用convertView,每次都new一个新的对象
- TextView leakedView = new TextView(LeakedListviewExample.this);
- leakedView.setText(String.valueOf(position));
- return leakedView;
- }
- }
- }

在运行代码的设备多次上下滑动这个ListView后,可以观察这个应用程序所占内存越来越大,每次手动GC后效果也不明显,说明出现了内存泄漏的现象,于是我们通过MAT dump出内存,切换到Dorminator Tree后可以看到如下的情况:
首先就可以看到ListView的shallow heap和retained heap是非常不成比例的,这说明这个ListView持有了大量对象的引用,正常情况下ListView是不会出现此现象。于是我们根据retained heap的大小逐步对ListView的支配树进行展开可以发现,Recycle Bin下面持有大量的TextView的引用,数量高达2500多个,而在正常情况下,ListView会复用Recycle Bin当中type类型相同的view,其中的对象个数不可能如此庞大,因此,从这里就可以断定ListView使用不当出现了内存泄漏。
再看 Histogram
以直方图的方式来显示当前内存使用情况可能更加适合较为复杂的内存泄漏分析,它默认直接显示当前内存中各种类型对象的数量及这些对象的shallow heap和retained heap。结合MAT提供的不同显示方式,往往能够直接定位问题,还是以上面的代码为例,当我们切换到histogram视图下时,可以看到大量的TextView的引用,数量高达2500多个
根据Retained heap进行排序后可见,TextView存在2539个对象,消耗的内存也比较靠前,由于TextView属于布局中的组件,一般来说应用程序界面不能复杂到有这么多的TextView的存在,因此这其中必定存在问题。那么又如何来确定到底是谁在持有这些对象而导致内存没有被回收呢?有两种方式,第一就是右键选择List Objects -> with incoming reference,这可以列出所有持有这些TextView对象的引用路径,如图
从中可以看出几乎所有的TextView对象都被ListView持有,因此基本可以断定出现了ListView没有复用view的情况;第二种方式就更直接粗暴,但是非常有效果,那就是借助我们刚才说的dorminator tree,右键选择Immediate Dorminator后就可以看到如下结果
从中可以看到,有2508个TextView对象都被ListView的RecycleBin持有,原因自然就很明确了。
- package com.example.demo;
-
- import android.content.Context;
-
- /**
- * @created on: 2021/4/5 22:09
- * @project: demo
- */
- public class MySingelTest {
- private static MySingelTest mInstance;
- private static Context mContext;
- private static final Object SYNC_LOCK = new Object();
-
- private MySingelTest(Context context) {
- mContext = context;
- }
-
- public static MySingelTest getInstance(Context context) {
- if (null == mInstance) {
- synchronized (SYNC_LOCK) {
- if (null == mInstance) {
- mInstance = new MySingelTest(context);
- }
- }
- }
- return mInstance;
- }
-
- public void setCustomText() {
- MyTextView textView = new MyTextView(mContext);
- textView.setText("ffff");
- }
- }
-

2.Activity使用方
- package com.example.demo;
-
- import androidx.appcompat.app.AppCompatActivity;
-
- import android.os.Bundle;
-
- public class MainActivity extends AppCompatActivity {
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- MySingelTest.getInstance(this).setCustomText();
- }
-
- }

说明:此种用法会导致Activity不能释放,而导致内存泄漏,单例好像只会造成2个Activity内存泄漏
3.查看hprof文件
可以看到 :
可以看到 :
4.综上所得:
因为MySingleTest 是单例, MySingleTest 中的mContext 变量持有 MainActivity, 导致 MainActivity 实例不能释放,因此导致了内存泄漏的发生
5.解决方案
上下文不要使用当前Activity的上下文,要使用Application的上下文;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。