原文出处: 高建武 (Granker,@高爷) php
MAT简html
MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它能够帮助咱们查找内存泄漏和减小内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工做,并能够经过报表直观的查看到可能形成这种结果的对象。java
MATandroid
固然MAT也有独立的不依赖Eclipse的版本,只不过这个版本在调试Android内存的时候,须要将DDMS生成的文件进行转换,才能够在独立版本的MAT上打开。不过Android SDK中已经提供了这个Tools,因此使用起来也是很方便的。c++
这里是MAT的下载地址:https://eclipse.org/mat/downloads.php,下载时会提供三种选择的方式:数据库
Download MAT数组
Update Site 这种方式后面会有一个网址:好比http://download.eclipse.org/mat/1.4/update-site/ ,安装过Eclipse插件的同窗应该知道,只要把这段网址复制到对应的Eclipse的Install New Software那里,就能够进行在线下载了。缓存
MAT with eclipse安全
Archived Update Site 这种方式安装的位置和上一种差很少,只不过第一种是在线下载,这一种是使用离线包进行更新,这种方式劣势是当这个插件更新后,须要从新下载离线包,而第一种方式则能够在线下载更新。dom
Stand-alone Eclipse RCP Applications 这种方式就是把MAT当成一个独立的工具使用,再也不依附于Eclipse,适合不使用Eclipse而使用Android Studio的同窗。这种方式有个麻烦的地方就是DDMS导出的文件,须要进行转换才能够在MAT中打开。
下载安装好以后,就可使用MAT进行实际的操做了。
使用MAT工具以前,要对Android的内存分配方式有基本的了解,对容易引发内存泄露的代码也要保持敏感,在代码级别对内存泄露的排查,有助于内存的使用。
Android主要应用在嵌入式设备当中,而嵌入式设备因为一些众所周知的条件限制,一般都不会有很高的配置,特别是内存是比较有限的。若是咱们编写的代码当中有太多的对内存使用不当的地方,不免会使得咱们的设备运行缓慢,甚至是死机。为了可以使得Android应用程序安全且快速的运行,Android的每一个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每一个应用程序都是在属于本身的进程中运行的。一方面,若是程序在运行过程当中出现了内存泄漏的问题,仅仅会使得本身的进程被kill掉,而不会影响其余进程(若是是system_process等系统进程出问题的话,则会引发系统重启)。另外一方面Android为不一样类型的进程分配了不一样的内存使用上限,若是应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉。
查询数据库没有关闭游标
描述:
程序中常常会进行查询数据库的操做,可是常常会有使用完毕Cursor后没有关闭的状况。若是咱们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操做的状况下才会复现内存问题,这样就会给之后的测试和问题排查带来困难和风险。
示例代码:
Java
1 2 3 4 |
Cursor cursor = getContentResolver().query(uri ...); if (cursor.moveToNext()) { ... ... } |
修正示例代码:
Java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Cursor cursor = null; try { cursor = getContentResolver().query(uri ...); if (cursor != null & cursor.moveToNext()) { ... ... } } finally { if (cursor != null) { try { cursor.close(); } catch (Exception e) { //ignore this } } } |
构造Adapter时,没有使用缓存的 convertView
描述:以构造ListView的BaseAdapter为例,在BaseAdapter中提供了方法:
Java
1 |
public View getView(int position, View convertView, ViewGroup parent) |
来向ListView提供每个item所须要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化必定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,而后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参 View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。
由此能够看出,若是咱们不去使用convertView,而是每次都在getView()中从新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用愈来愈大。ListView回收list item的view对象的过程能够查看:android.widget.AbsListView.java –> void addScrapView(View scrap) 方法。
示例代码:
Java
1 2 3 4 5 |
public View getView(int position, View convertView, ViewGroup parent) { View view = new Xxx(...); ... ... return view; } |
示例修正代码:
Java
1 2 3 4 5 6 7 8 9 10 11 12 |
public View getView(int position, View convertView, ViewGroup parent) { View view = null; if (convertView != null) { view = convertView; populate(view, getItem(position)); ... } else { view = new Xxx(...); ... } return view; } |
关于ListView的使用和优化,能够参这两篇文章:
Bitmap对象不在使用时调用recycle()释放内存
描述:有时咱们会手工的操做Bitmap对象,若是一个Bitmap对象比较占内存,当它不在被使用的时候,能够调用Bitmap.recycle()方法回收此对象的像素所占用的内存。
另外在最新版本的Android开发时,使用下面的方法也能够释放此Bitmap所占用的内存
Java
1 2 3 4 5 |
Bitmap bitmap ; ... bitmap初始化以及使用 ... bitmap = null; |
释放对象的引用
描述:这种状况描述起来比较麻烦,举两个例子进行说明。
示例A:
假设有以下操做
Java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class DemoActivity extends Activity { ... ... private Handler mHandler = ... private Object obj; public void operation() { obj = initObj(); ... [Mark] mHandler.post(new Runnable() { public void run() { useObj(obj); } }); } } |
咱们有一个成员变量 obj,在operation()中咱们但愿可以将处理obj实例的操做post到某个线程的MessageQueue中。在以上的代码中,即使是mHandler所在的线程使用完了obj所引用的对象,但这个对象仍然不会被垃圾回收掉,由于DemoActivity.obj还保有这个对象的引用。因此若是在DemoActivity中再也不使用这个对象了,能够在[Mark]的位置释放对象的引用,而代码能够修改成:
Java
1 2 3 4 5 6 7 8 9 10 11 |
public void operation() { obj = initObj(); ... final Object o = obj; obj = null; mHandler.post(new Runnable() { public void run() { useObj(o); } } } |
示例B:
假设咱们但愿在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则能够在LockScreen中定义一个PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当须要显示锁屏界面的时候就会建立一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。
可是若是在释放LockScreen对象的时候忘记取消咱们以前注册的PhoneStateListener对象,则会致使LockScreen没法被垃圾回收。若是不断的使锁屏界面显示和消失,则最终会因为大量的LockScreen对象没有办法被回收而引发OutOfMemory,使得system_process进程挂掉。
总之当一个生命周期较短的对象A,被一个生命周期较长的对象B保有其引用的状况下,在A的生命周期结束时,要在B中清除掉对A的引用。
其余
Android应用程序中最典型的须要注意释放资源的状况是在Activity的生命周期中,在onPause()、onStop()、onDestroy()方法中须要适当的释放资源的状况。因为此状况很基础,在此不详细说明,具体能够查看官方文档对Activity生命周期的介绍,以明确什么时候应该释放哪些资源。
要调试内存,首先须要获取HPROF文件,HPROF文件是MAT能识别的文件,HPROF文件存储的是特定时间点,java进程的内存快照。有不一样的格式来存储这些数据,总的来讲包含了快照被触发时java对象和类在heap中的状况。因为快照只是一瞬间的事情,因此heap dump中没法包含一个对象在什么时候、何地(哪一个方法中)被分配这样的信息。
这个文件可使用DDMS导出,DDMS中在Devices上面有一排按钮,选择一个进程后(即在Devices下面列出的列表中选择你要调试的应用程序的包名),点击Dump HPROF file 按钮:
Dump HEAP with DDMS
选择存储路径保存后就能够获得对应进程的HPROF文件。eclipse插件能够把上面的工做一键完成。只须要点击Dump HPROF file图标,而后MAT插件就会自动转换格式,而且在eclipse中打开分析结果。eclipse中还专门有个Memory Analysis视图 ,获得对应的文件后,若是安装了Eclipse插件,那么切换到Memory Analyzer视图。使用独立安装的,要使用Android SDK自带的的工具(hprof-conv 位置在sdk/platform-tools/hprof-conv)进行转换
Java
1 |
hprof-conv xxx.xxx.xxx.hprof xxx.xxx.xxx.hprof |
转换事后的.hprof文件便可使用MAT工具打开了。
使用Android Studio一样能够导出对应的HPROF文件:
Android-Studio
最新版本的Android Studio得在文件上右键转换成标准的HPROF文件,在能够在MAT中打开。
这里介绍的不是MAT这个工具的主界面,而是导入一个文件以后,显示OverView的界面。
打开通过转换的hprof文件:
open hprof
若是选择了第一个,则会生成一个报告。这个无大碍。
Leak Suspects
选择OverView界面:
System OverView
咱们须要关注的是下面的Actions区域
Dominator Tree
Top Consumers : 经过图形列出最大的object
Top Consumers
Duplicate Class:经过MAT自动分析泄漏的缘由
Histogram:列出内存中的对象,对象的个数以及大小
Histogram
Dominator Tree:列出最大的对象以及其依赖存活的Object (大小是以Retained Heap为标准排序的)
通常Histogram和 Dominator Tree是最经常使用的。
要看懂MAT的列表信息,Shallow heap、Retained Heap、GC Root这几个概念必定要弄懂。
Shallow size就是对象自己占用内存的大小,不包含其引用的对象。
常规对象(非数组)的Shallow size有其成员变量的数量和类型决定。
数组的shallow size有数组元素的类型(对象类型、基本类型)和数组长度决定
由于不像c++的对象自己能够存放大量内存,java的对象成员都是些引用。真正的内存都在堆上,看起来是一堆原生的byte[], char[], int[],因此咱们若是只看对象自己的内存,那么数量都很小。因此咱们看到Histogram图是以Shallow size进行排序的,排在第一位第二位的是byte,char 。
Retained Heap的概念,它表示若是一个对象被释放掉,那会由于该对象的释放而减小引用进而被释放的全部的对象(包括被递归释放的)所占用的heap大小。因而,若是一个对象的某个成员new了一大块int数组,那这个int数组也能够计算到这个对象中。相对于shallow heap,Retained heap能够更精确的反映一个对象实际占用的大小(由于若是该对象释放,retained heap均可以被释放)。
这里要说一下的是,Retained Heap并不老是那么有效。例如我在A里new了一块内存,赋值给A的一个成员变量。此时我让B也指向这块内存。此时,由于A和B都引用到这块内存,因此A释放时,该内存不会被释放。因此这块内存不会被计算到A或者B的Retained Heap中。为了纠正这点,MAT中的Leading Object(例如A或者B)不必定只是一个对象,也能够是多个对象。此时,(A, B)这个组合的Retained Set就包含那块大内存了。对应到MAT的UI中,在Histogram中,能够选择Group By class, superclass or package来选择这个组。
为了计算Retained Memory,MAT引入了Dominator Tree。加入对象A引用B和C,B和C又都引用到D(一个菱形)。此时要计算Retained Memory,A的包括A自己和B,C,D。B和C由于共同引用D,因此他俩的Retained Memory都只是他们自己。D固然也只是本身。我以为是为了加快计算的速度,MAT改变了对象引用图,而转换成一个对象引用树。在这里例子中,树根是A,而B,C,D是他的三个儿子。B,C,D再也不有相互关系。把引用图变成引用树,计算Retained Heap就会很是方便,显示也很是方便。对应到MAT UI上,在dominator tree这个view中,显示了每一个对象的shallow heap和retained heap。而后能够以该节点位树根,一步步的细化看看retained heap究竟是用在什么地方了。要说一下的是,这种从图到树的转换确实方便了内存分析,但有时候会让人有些疑惑。原本对象B是对象A的一个成员,但由于B还被C引用,因此B在树中并不在A下面,而极可能是平级。
为了纠正这点,MAT中点击右键,能够List objects中选择with outgoing references和with incoming references。这是个真正的引用图的概念,
outgoing references :表示该对象的出节点(被该对象引用的对象)。
incoming references :表示该对象的入节点(引用到该对象的对象)。
为了更好地理解Retained Heap,下面引用一个例子来讲明:
把内存中的对象当作下图中的节点,而且对象和对象之间互相引用。这里有一个特殊的节点GC Roots,这就是reference chain(引用链)的起点:
Paste_Image.png
Paste_Image.png
从obj1入手,上图中蓝色节点表明仅仅只有经过obj1才能直接或间接访问的对象。由于能够经过GC Roots访问,因此左图的obj3不是蓝色节点;而在右图倒是蓝色,由于它已经被包含在retained集合内。
因此对于左图,obj1的retained size是obj一、obj二、obj4的shallow size总和;
右图的retained size是obj一、obj二、obj三、obj4的shallow size总和。
obj2的retained size能够经过相同的方式计算。
GC发现经过任何reference chain(引用链)没法访问某个对象的时候,该对象即被回收。名词GC Roots正是分析这一过程的起点,例如JVM本身确保了对象的可到达性(那么JVM就是GC Roots),因此GC Roots就是这样在内存中保持对象可到达性的,一旦不可到达,即被回收。一般GC Roots是一个在current thread(当前线程)的call stack(调用栈)上的对象(例如方法参数和局部变量),或者是线程自身或者是system class loader(系统类加载器)加载的类以及native code(本地代码)保留的活动对象。因此GC Roots是分析对象为什么还存活于内存中的利器。
Thread OvewView能够查看这个应用的Thread信息:
Thread OvewView
在Histogram和Domiantor Tree界面,能够选择将结果用另外一种Group的方式显示(默认是Group by Object),切换到Group by package,能够更好地查看具体是哪一个包里的类占用内存大,也很容易定位到本身的应用程序。
Group
在Histogram或者Domiantor Tree的某一个条目上,右键能够查看其GC Root Path:
Path to GC Root
这里也要说明一下Java的引用规则:
从最强到最弱,不一样的引用(可到达性)级别反映了对象的生命周期。
Strong Ref(强引用):一般咱们编写的代码都是Strong Ref,于此对应的是强可达性,只有去掉强可达,对象才被回收。
Soft Ref(软引用):对应软可达性,只要有足够的内存,就一直保持对象,直到发现内存吃紧且没有Strong Ref时才回收对象。通常可用来实现缓存,经过java.lang.ref.SoftReference类实现。
Weak Ref(弱引用):比Soft Ref更弱,当发现不存在Strong Ref时,马上回收对象而没必要等到内存吃紧的时候。经过java.lang.ref.WeakReference和java.util.WeakHashMap类实现。
Phantom Ref(虚引用):根本不会在内存中保持任何对象,你只能使用Phantom Ref自己。通常用于在进入finalize()方法后进行特殊的清理过程,经过 java.lang.ref.PhantomReference实现。
点击Path To GC Roots –> with all references
Path To GC Roots
http://cxy.liuzhihengseo.com/529.html QQ技术交流群290551701