这篇文章主要 分为四部分来具体讲解:
1.HeapSnapShot使用
2.HeapViewer使用
3.MAT使用
4.实际开发中怎么去运用上面三个工具来寻找可能发生内存泄漏的代码。
第一部分:HeapSnapShot使用:
Heap Snapshot能做什么?
获取Java堆内存详细信息,可以分析出内存泄漏的问题
1. 启动Heap Dump
2. 生成Dump分析结果
该面板里的信息可以有三种类型:app heap/image heap/zygote heap.
分别代表app 堆内存信息,图片堆内存信息,zygote进程的堆内存信息。
A区域:
列举了堆内存中所有的类,一下是列表中列名:
名称 意义
Total Count 内存中该类的对象个数
Heap Count 堆内存中该类的对象个数
Sizeof 物理大小
Shallow size 该对象本身占有内存大小
Retained Size 释放该对象后,节省的内存大小
B区域:
当我们点击某个类时,右边的B区域会显示该类的实例化对象,这里面会显示有多少个实体,以及详细信息。
名称 意义
depth 深度
Shallow Size 对象本身内存大小
Dominating Size 管辖的内存大小
C区域:
当你点击B区域某个对象时,将展开该对象内部含有哪些对象,同时C区域也会显示哪些对象引用了该对象:
这里的Commutils的context就是引用的MainActivity。
Androidstudio新面板:
较之前添加了一个数据显示方式的选择,可以选择类名排列或者包名排列:
第二部分:HeapViewer使用:
Heap Viewer能做什么?
• 实时查看App分配的内存大小和空闲内存大小
• 发现Memory Leaks
Heap Viewer使用条件
• 5.0以上的系统,包括5.0
• 开发者选项可用
Heap Viewer启动
Heap Viewer面板:
按上图的标记顺序按下,我们就能看到内存的具体数据,右边面板中数值会在每次GC时发生改变,包括App自动触发或者你来手动触发。
总览:
列名 意义
Heap Size 堆栈分配给App的内存大小
Allocated 已分配使用的内存大小
Free 空闲的内存大小
%Used Allocated/Heap Size,使用率
Objects 对象数量
详情
类型 意义
free 空闲的对象
data object 数据对象,类类型对象,最主要的观察对象
class object 类类型的引用对象
1-bytearray(byte[],boolean[]) 一个字节的数组对象
2-bytearray(short[],char[]) 两个字节的数组对象
4-bytearray(long[],double[]) 4个字节的数组对象
non-Java object 非Java对象
下面是每一个对象都有的列名含义:
列名 意义
Count 数量
Total Size 总共占用的内存大小
Smallest 将对象占用内存的大小从小往大排,排在第一个的对象占用内存大小
Largest 将对象占用内存的大小从小往大排,排在最后一个的对象占用的内存大小
Median 将对象占用内存的大小从小往大排,拍在中间的对象占用的内存大小
Average 平均值
横坐标是对象的内存大小,这些值随着不同对象是不同的,纵坐标是在某个内存大小上的对象的数量.
我们说Heap Viewer适合发现内存泄漏的问题,那么如何检测呢?
HeapViewer中的数值会自动在每次发生GC时会自动更新,那么我们是等着他自己GC么?既然我们是来看内存泄漏,那么我们在需要检测内存泄漏的用例执行过后,手动GC下,然后观察data object一栏的total size(也可以观察Heap Size/Allocated内存的情况),看看内存是不是会回到一个稳定值,多次操作后,只要内存是稳定在某个值,那么说明没有内存溢出的,如果发现内存在每次GC后,都在增长,不管是慢增长还是快速增长,都说明有内存泄漏的可能性。
补充
Heap Viewer不光可以用来检测是否有内存泄漏,对于内存抖动,我们也可以用该工具检测,因为内存抖动的时候,会频繁发生GC,这个时候我们只需要开启Heap Viewer,观察数据的变化,如果发生内存抖动,会观察到数据在段时间内频繁更新
第三部分:MAT分析工具使用:
首先去下载:http://eclipse.org/mat/downloads.php
MAT工具全称为MemoryAnalyzer Tool,一款详细分析Java堆内存的工具,该工具非常强大,为了使用该工具,我们需要hprof文件。但是该文件不能直接被MAT使用,需要进行一步转化,可以使用hprof-conv命令来转化,但是Android Studio可以直接转化,转化方法如下:
1. 选择一个hprof文件,点击右键选择Export to standard .hprof选项。
2. 填写更改后的文件名和路径:
点击OK按钮后,MAT工具所需的文件就生成了,下面我们用MAT来打开该工具:
3. 打开MAT后选择File->Open File选择我们刚才生成的doctorq.hprof文件
4.选择该文件后,MAT会有几秒种的时间解析该文件,有的hprof文件可能过大,会有更长的时间解析,解析后,展现在我们的面前的界面如下:
这是个总览界面,会大体给出一些分析后初步的结论.
Overview视图:
该视图会首页总结出当前这个Heapdump占用了多大的内存,其中涉及的类有多少,对象有多少,类加载器,如果有没有回收的对象,会有一个连接,可以直接参看(图中的Unreachable Objects Histogram)。
比如该例子中显示了Heap dump占用了34.6M的内存,5.5K个类,98.3k个对象,6个类加载器。
然后还会有各种分类信息:
BiggestObjects by Retained Size
会列举出Retained Size值最大的几个值,你可以将鼠标放到饼图中的扇叶上,可以在右侧看出详细信息:
图中灰色区域,并不是我们需要关心的,他是除了大内存对象外的其他对象,我们需要关心的就是图中彩色区域,比如图中2.4M的对象,我们来看看该对象到底是啥:
该对象是一个Bitmap对象,你如果想知道该对象到底是什么图片,可以使用图片工具gimp工具浏览该对象.
histogram视图
histogram视图主要是查看某个类的实例个数,比如我们在检查内存泄漏时候,要判断是否频繁创建了对象,就可以来看对象的个数来看。也可以通过排序看出占用内存大的对象:
默认是类名形式展示,你也可以选择不同的显示方式,有以下四种方式:
Dominator tree视图
该视图会以占用总内存的百分比来列举所有实例对象,注意这个地方是对象而不是类了,这个视图是用来发现大内存对象的。这些对象都可以展开查看更详细的信息,可以看到该对象内部包含的对象:
Leaks suspects视图
这个视图会展示一些可能的内存泄漏的点,比如上图上图显示有3个内存泄漏可疑点,我们以Problem Suspect 1为例来理解该报告,首先我们来看该可疑点详细信息:
上面信息显示ImageCahe类的一个实例0xa50819f8占用了14.19%的内存,具体值为5147200字节(5147200/1024/1024=4.9M),并存放在LinkedHashMap这个集合中,然后我们点击Details跳转到更详细的页面:
这样我们就能找到在我们的app源码中造成该泄漏可疑点的地方,很容易去定位问题。
第四部分:实际案例结合工具使用
上面光是说了如何去使用某个单个工具,但是往往做项目的时候情况非常复杂,或者项目做得差不多了想起来要性能优化检查下内存泄露。
那如何找到项目中存在的内存泄露的这些地方呢?
下面是我常用的方法:
1.确定是否存在内存泄露
1)Android Monitors的内存分析(常用)
最直观的看内存增长情况,知道该动作是否发生内存泄露。
动作发生之前:GC完后内存1.4M; 动作发生之后:GC完后内存1.6M
2)使用MAT内存分析工具
MAT分析heap的总内存占用大小来初步判断是否存在泄露
Heap视图中有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。
在data object一行中有一列是“TotalSize”,其值就是当前进程中所有Java数据对象的内存总量,
一般情况下,这个值的大小决定了是否会有内存泄漏。
我们反复执行某一个操作并同时执行GC排除可以回收掉的内存,注意观察data object的Total Size值,
正常情况下TotalSize值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾 回收的情况。
反之如果代码中存在没有释放对象引用的情况,随着操作次数的增多Total Size的值会越来越大。
那么这里就已经初步判断这个操作导致了内存泄露的情况。
2.先找怀疑对象(哪些对象属于泄露的)
MAT对比操作前后的hprof来定位内存泄露是泄露了什么数据对象。(这样做可以排除一些对象,不用后面去查看所有被引用的对象是否是嫌疑)
快速定位到操作前后所持有的对象哪些是增加了(GC后还是比之前多出来的对象就可能是泄露对象嫌疑犯)
技巧:Histogram中还可以对对象进行Group,比如选择Group ByPackage更方便查看自己Package中的对象信息。
3. MAT分析hprof来定位内存泄露的原因所在。(哪个对象持有了上面怀疑出来的发生泄露的对象)
1)Dump出内存泄露“当时”的内存镜像hprof,分析怀疑泄露的类;
2)把上面2得出的这些嫌疑犯一个一个排查个遍。步骤:
(1)进入Histogram,过滤出某一个嫌疑对象类
(2)然后分析持有此类对象引用的外部对象(在该类上面点击右键List Objects--->with incoming references)
(3)再过滤掉一些弱引用、软引用、虚引用,因为它们迟早可以被GC干掉不属于内存泄露
(在类上面点击右键MergeShortest Paths to GC Roots--->exclude all phantom/weak/soft etc.references)
(4)逐个分析每个对象的GC路径是否正常
此时就要进入代码分析此时这个对象的引用持有是否合理,这就要考经验和体力了!
在这之前需要举个例子:
CommonUtils.java:
public class CommUtils { private static CommUtils instance; private Context context; private CommUtils(Context context) { this.context = context; } public static CommUtils getInstance(Context mContext) { if (instance == null) { instance = new CommUtils(mContext); } return instance; } public void setContext(Context context) { this.context = context; } }
MainActivity.java:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); CommUtils commUtil = CommUtils.getInstance(this); } }相信很多人已经看出来哪边出了内存泄漏,但这就是我们项目中很多人都写过的代码,但是很多人都没有注意到。
接下来我就用图来一步一步解析一下我是如何用的:
首先利用Android Studio dump下旋转屏幕先后的dump标准文件
这里我们命名为旋转前为before.prof,旋转屏幕后为now.prof,注意生成这些文件之前需要去手动gc下内存,排除可以被GC的内存。
然后通过MAT将两个文件进行对比分析(两个文件都这样):
这时会生成一个对比的结果:
有人问这么多东西难道要一个个看,其实不用我们只需要关注我们自己项目的代码:
这里会发现在旋转前后MainActivity的对象数量发生了变化,并且没有被GC掉,所以这里可能发生了内存泄漏,列为怀疑对象。
接下来我们回到now.hprof的histogram列表:
找到怀疑的对象的内存信息:
接着右击List objects > with incoming references查看是哪些对象引用了MainActivity:
展开列表:
这里内部存在着很多对象的引用,但是这里面存在着许多虚引用、软引用、弱引用,所以我们要排除这些障碍:
右击Merge Shortest Paths to GC Roots > excludeall phantom/weak/soft etc
这样可以过滤掉这些:
这里可以看到可能是Commutils引用了MainActivity,导致MainActivity,导致Mainactivity旋转后不能销毁。
以上就是MAT结合实际案例的使用方法。