【转】Android 分析内存的使用情况

标签: android 分析 内存 | 发表时间:2015-12-12 23:08 | 作者:gdfdfg
出处:http://www.iteye.com

转载请注明出处: http://blog.csdn.net/guolin_blog/article/details/42238633

由于Android是为移动设备开发的操作系统,我们在开发应用程序的时候应当始终把内存问题充分考虑在内。虽然Android系统拥有垃圾自动回 收机制,但这并不意味着我们就可以完全忽略何时去分配或释放内存。即使我们全部按照上一篇文章中给出的编程建议来去编写程序,还是会很有可能出现内存泄露 或其它类型的内存问题。所以,唯一能够解决问题的办法,就是尝试去分析应用程序的内存使用情况,那么本篇文章就会教大家如何进行分析。如果你还没有看过前 面一篇文章,建议先去阅读 Android最佳性能实践(一)——合理管理内存 。

虽说现在的手机内存都已经非常大了,但是我们大家都知道,系统是不可能将所有的内存都分配给我们的应用程序的。没错,每个程序都会有可使用的内存上 限,这被称为堆大小(Heap Size)。不同的手机,堆大小也不尽相同,随着现在硬件设备不断提高,堆大小也已经由Nexus One时的32MB,变成了Nexus 5时的192MB。如果大家想要知道自己手机的堆大小是多少,可以调用如下代码:

  1. ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);  
  2. int heapSize = manager.getLargeMemoryClass();  
结果是以MB为单位进行返回的,我们在开发应用程序时所使用的内存不能超出这个限制,否则就会出现OutOfMemoryError。因此,比如说我们的程序中需要缓存一些数据,就可以根据堆大小来决定缓存数据的容量。

下面我们来讨论一下Android的GC操作,GC全称是Garbage Collection,也就是所谓的垃圾回收。Android系统会在适当的时机触发GC操作,一旦进行GC操作,就会将一些不再使用的对象进行回收。那 么哪些对象会被认为是不再使用,并且可以被回收的呢?我们来看下面一张图:

上图当中,每个蓝色的圆圈就代表一个内存当中的对象,而圆圈之间的箭头就是它们的引用关系。这些对象有些是处于活动状态的,而有些就已经不再被使用 了。那么GC操作会从一个叫作Roots的对象开始检查,所有它可以访问到的对象就说明还在使用当中,应该进行保留,而其它的对象就表示已经不再被使用 了,如下图所示:

可以看到,目前所有黄色的对象仍然会被系统继续保留,而蓝色的对象就会在GC操作当中被系统回收掉了,这大概就是Android系统一次简单的GC流程。

那么什么时候会触发GC操作呢?这个通常都是由系统去决定的,我们一般情况下都不需要主动通知系统应该去GC了(虽然我们确实可以这么做,下面会讲 到),但是我们仍然可以去监听系统的GC过程,以此来分析我们应用程序当前的内存状态。那么怎样才能去监听系统的GC过程呢?其实非常简单,系统每进行一 次GC操作时,都会在LogCat中打印一条日志,我们只要去分析这条日志就可以了,日志的基本格式如下所示:

  1. D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,  <Pause_time>  
注意这里我仍然是以dalvik虚拟机来进行说明,art情况下打印的内容也是基本类似的。。

首先第一部分 GC_Reason,这个是触发这次GC操作的原因,一般情况下一共有以下几种触发GC操作的原因:

  • GC_CONCURRENT:   当我们应用程序的堆内存快要满的时候,系统会自动触发GC操作来释放内存。
  • GC_FOR_MALLOC:   当我们的应用程序需要分配更多内存,可是现有内存已经不足的时候,系统会进行GC操作来释放内存。
  • GC_HPROF_DUMP_HEAP:   当生成HPROF文件的时候,系统会进行GC操作,关于HPROF文件我们下面会讲到。
  • GC_EXPLICIT:   这种情况就是我们刚才提到过的,主动通知系统去进行GC操作,比如调用System.gc()方法来通知系统。或者在DDMS中,通过工具按钮也是可以显式地告诉系统进行GC操作的。

接下来第二部分 Amount_freed,表示系统通过这次GC操作释放了多少内存。

然后 Heap_stats中会显示当前内存的空闲比例以及使用情况(活动对象所占内存 / 当前程序总内存)。

最后 Pause_time表示这次GC操作导致应用程序暂停的时间。关于这个暂停的时间,Android在 2.3的版本当中进行过一次优化,在2.3之前GC操作是不能并发进行的,也就是系统正在进行GC,那么应用程序就只能阻塞住等待GC结束。虽说这个阻塞 的过程并不会很长,也就是几百毫秒,但是用户在使用我们的程序时还是有可能会感觉到略微的卡顿。而自2.3之后,GC操作改成了并发的方式进行,就是说 GC的过程中不会影响到应用程序的正常运行,但是在GC操作的开始和结束的时候会短暂阻塞一段时间,不过优化到这种程度,用户已经是完全无法察觉到了。

下面是一次GC操作在LogCat中打印的日志:

可以看出,和我们上面所介绍的格式是完全一致的,最后的暂停时间31ms+7ms,一次就是GC开始时的暂停时间,一次是结束时的暂停时间。另外可以根据进程id来区分这是哪个程序中进行的GC操作,那么从上图就可以看出这条GC日志是属于24699这个程序的。

那么这是使用dalvik运行环境时所打印的GC日志,而自Android 4.4版本之后加入了art运行环境,在art中打印GC日志基本和dalvik是相同的,如下图所示:

相信没有什么难理解的地方吧,art中只是内容显示的格式有了稍许变化,打印的主体内容仍然是不变的。

好的,通过日志的方式我们可以简单了解到系统的GC工作情况,但是如果我们想要更加清楚地实时知晓当前应用程序的内存使用情况,只通过日志就有些力不从心了,我们需要通过DDMS中提供的工具来实现。

打开DDMS界面,在左侧面板中选择你要观察的应用程序进程,然后点击 Update Heap按钮,接着在右侧面板中点击Heap标签,之后不停地点击Cause GC按钮来实时地观察应用程序内存的使用情况即可,如下图所示:

接着继续操作我们的应用程序,然后继续点击 Cause GC按钮,如果你发现反复操作某一功能会导致应用程序内存持续增高而不会下降的话,那么就说明这里很有可能发生内存泄漏了。

好了,讨论完了GC,接下来我们讨论一下Android中内存泄漏的问题。大家需要知道的是,Android中的垃圾回收机制并不能防止内存泄漏的 出现,导致内存泄漏最主要的原因就是某些长存对象持有了一些其它应该被回收的对象的引用,导致垃圾回收器无法去回收掉这些对象,那也就出现内存泄漏了。比 如说像Activity这样的系统组件,它又会包含很多的控件甚至是图片,如果它无法被垃圾回收器回收掉的话,那就算是比较严重的内存泄漏情况了。

下面我们来模拟一种Activity内存泄漏的场景,内部类相信大家都有用过,如果我们在一个类中又定义了一个非静态的内部类,那么这个内部类就会持有外部类的引用,如下所示:

  1. public class MainActivity extends ActionBarActivity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.         LeakClass leakClass = new LeakClass();  
  8.     }  
  9.   
  10.     class LeakClass {  
  11.   
  12.     }  
  13.     ......  
  14. }  
目前来看,代码还是没有问题的,因为虽然LeakClass这个内部类持有MainActivity的引用,但是只要它的存活时间不会长于MainActivity,就不会阻止MainActivity被垃圾回收器回收。那么现在我们来将代码进行如下修改:
  1. public class MainActivity extends ActionBarActivity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.         LeakClass leakClass = new LeakClass();  
  8.         leakClass.start();  
  9.     }  
  10.   
  11.     class LeakClass extends Thread {  
  12.   
  13.         @Override  
  14.         public void run() {  
  15.             while (true) {  
  16.                 try {  
  17.                     Thread.sleep(60 * 60 * 1000);  
  18.                 } catch (InterruptedException e) {  
  19.                     e.printStackTrace();  
  20.                 }  
  21.             }  
  22.         }  
  23.     }  
  24.     ......  
  25. }  
这下就有点不太一样了,我们让LeakClass继承自Thread,并且重写了run()方法,然后在MainActivity的onCreate() 方法中去启动LeakClass这个线程。而LeakClass的run()方法中运行了一个死循环,也就是说这个线程永远都不会执行结束,那么 LeakClass这个对象就一直不能得到释放,并且它持有的MainActivity也将无法得到释放,那么内存泄露就出现了。

现在我们可以将程序运行起来,然后不断地旋转手机让程序在横屏和竖屏之间切换,因为每切换一次Activity都会经历一个重新创建的过程,而前面 创建的Activity又无法得到回收,那么长时间操作下我们的应用程序所占用的内存就会越来越高,最终出现OutOfMemoryError。

下面我贴出一张不断切换横竖屏时GC日志打印的结果图,如下所示:

可以看到,应用程序所占用的内存是在不断上升的。最可怕的是,这些内存一旦升上去了就永远不会再降下来,直到程序崩溃为止,因为这部分泄露的内存一直都无法被垃圾回收器回收掉。

那么通过上面学习的GC日志以及DDMS工具这两种方式,现在我们已经可以比较轻松地发现应用程序中是否存在内存泄露的现象了。但是如果真的出现了 内存泄露,我们应该怎么定位到具体是哪里出的问题呢?这就需要借助一个内存分析工具了,叫做Eclipse Memory Analyzer(MAT)。我们需要先将这个工具下载下来,下载地址是: http://eclipse.org/mat/downloads.php。这个工具分为Eclipse插件版和独立版两种,如果你是使用Eclipse开发的,那么可以使用插件版MAT,非常方便。如果你是使用Android Studio开发的,那么就只能使用独立版的MAT了。

下载好了之后下面我们开始学习如何去分析内存泄露的原因,首先还是进入到DDMS界面,然后在左侧面板选中我们要观察的应用程序进程,接着点击 Dump HPROF file按钮,如下图所示:

点击这个按钮之后需要等待一段时间,然后会生成一个HPROF文件,这个文件记录着我们应用程序内部的所有数据。但是目前MAT还是无法打开这个文 件的,我们还需要将这个HPROF文件从Dalvik格式转换成J2SE格式,使用hprof-conv命令就可以完成转换工作,如下所示:

  1. hprof-conv dump.hprof converted-dump.hprof  

hprof-conv命令文件存放于<Android Sdk>/platform-tools目录下面。另外如果你是使用的插件版的MAT,也可以直接在Eclipse中打开生成的HPROF文件,不用经过格式转换这一步。

好的,接下来我们就可以来尝试使用MAT工具去分析内存泄漏的原因了,这里需要提醒大家的是,MAT并不会准确地告诉我们哪里发生了内存泄漏,而是 会提供一大堆的数据和线索,我们需要自己去分析这些数据来去判断到底是不是真的发生了内存泄漏。那么现在运行MAT工具,然后选择打开转换过后的 converted-dump.hprof文件,如下图所示:

MAT中提供了非常多的功能,这里我们只要学习几个最常用的就可以了。上图最中央的那个饼状图展示了最大的几个对象所占内存的比例,这张图中提供的内容并不多,我们可以忽略它。在这个饼状图的下方就有几个非常有用的工具了,我们来学习一下。

Histogram可以列出内存中每个对象的名字、数量以及大小。

Dominator Tree会将所有内存中的对象按大小进行排序,并且我们可以分析对象之间的引用结构。

一般最常用的就是以上两个功能了,那么我们先从Dominator Tree开始学起。

现在点击Dominator Tree,结果如下图所示:

这张图包含的信息非常多,我来带着大家一起解析一下。首先Retained Heap表示这个对象以及它所持有的其它引用(包括直接和间接)所占的总内存,因此从上图中看,前两行的Retained Heap是最大的,我们分析内存泄漏时,内存最大的对象也是最应该去怀疑的。

另外大家应该可以注意到,在每一行的最左边都有一个文件型的图标,这些图标有的左下角带有一个红色的点,有的则没有。带有红点的对象就表示是可以被 GC Roots访问到的,根据上面的讲解,可以被GC Root访问到的对象都是无法被回收的。那么这就说明所有带红色的对象都是泄漏的对象吗?当然不是,因为有些对象系统需要一直使用,本来就不应该被回收。 我们可以注意到,上图当中所有带红点的对象最右边都有写一个System Class,说明这是一个由系统管理的对象,并不是由我们自己创建并导致内存泄漏的对象。

那么上图中就无法看出内存泄漏的原因了吗?确实,内存泄漏本来就不是这么容易找出的,我们还需要进一步进行分析。上图当中,除了带有System Class的行之外,最大的就是第二行的Bitmap对象了,虽然Bitmap对象现在不能被GC Roots访问到,但不代表着Bitmap所持有的其它引用也不会被GC Roots访问到。现在我们可以对着第二行点击右键 -> Path to GC Roots -> exclude weak references,为什么选择exclude weak references呢?因为弱引用是不会阻止对象被垃圾回收器回收的,所以我们这里直接把它排除掉,结果如下图所示:

可以看到,Bitmap对象经过层层引用之后,到了MainActivity$LeakClass这个对象,然后在图标的左下角有个红色的图标,就 说明在这里可以被GC Roots访问到了,并且这是由我们自己创建的Thread,并不是System Class了,那么由于MainActivity$LeakClass能被GC Roots访问到导致不能被回收,导致它所持有的其它引用也无法被回收了,包括MainActivity,也包括MainActivity中所包含的图 片。

通过这种方式,我们就成功地将内存泄漏的原因找出来了。这是Dominator Tree中比较常用的一种分析方式,即搜索大内存对象通向GC Roots的路径,因为内存占用越高的对象越值得怀疑。

接下来我们再来学习一下Histogram的用法,回到Overview界面,点击Histogram,结果如下图所示:

这里是把当前应用程序中所有的对象的名字、数量和大小全部都列出来了,需要注意的是,这里的对象都是只有Shallow Heap而没有Retained Heap的,那么Shallow Heap又是什么意思呢?就是当前对象自己所占内存的大小,不包含引用关系的,比如说上图当中,byte[]对象的Shallow Heap最高,说明我们应用程序中用了很多byte[]类型的数据,比如说图片。可以通过右键 -> List objects -> with incoming references来查看具体是谁在使用这些byte[]。

那么通过Histogram又怎么去分析内存泄漏的原因呢?当然其实也可以用和Dominator Tree中比较相似的方式,即分析大内存的对象,比如上图中byte[]对象内存占用很高,我们通过分析byte[],最终也是能找到内存泄漏所在的,但 是这里我准备使用另外一种更适合Histogram的方式。大家可以看到,Histogram中是可以显示对象的数量的,那么比如说我们现在怀疑 MainActivity中有可能存在内存泄漏,就可以在第一行的正则表达式框中搜索“MainActivity”,如下所示:

可以看到,这里将包含“MainActivity”字样的所有对象全部列出了出来,其中第一行就是MainActivity的实例。但是大家有没有 注意到,当前内存中是有11个MainActivity的实例的,这太不正常了,通过情况下一个Activity应该只有一个实例才对。其实这些对象就是 由于我们刚才不断地横竖屏切换所产生的,因为横竖屏切换一次,Activity就会经历一个重新创建的过程,但是由于LeakClass的存在,之前的 Activity又无法被系统回收,那么就出现这种一个Activity存在多个实例的情况了。

接下来对着MainActivity右键 -> List objects -> with incoming references查看具体MainActivity实例,如下图所示:

如果想要查看内存泄漏的具体原因,可以对着任意一个MainActivity的实例右键 -> Path to GC Roots -> exclude weak references,结果如下图所示:

可以看到,我们再次找到了内存泄漏的原因,是因为MainActivity$LeakClass对象所导致的。

好了,这大概就是MAT工具最常用的一些用法了,当然这里还要提醒大家一句,工具是死的,人是活的,MAT也没有办法保证一定可以将内存泄漏的原因 找出来,还是需要我们对程序的代码有足够多的了解,知道有哪些对象是存活的,以及它们存活的原因,然后再结合MAT给出的数据来进行具体的分析,这样才有 可能把一些隐藏得很深的问题原因给找出来。



已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [android 分析 内存] 推荐:

【转】Android 分析内存的使用情况

- - 研发管理 - ITeye博客
由于Android是为移动设备开发的操作系统,我们在开发应用程序的时候应当始终把内存问题充分考虑在内. 虽然Android系统拥有垃圾自动回 收机制,但这并不意味着我们就可以完全忽略何时去分配或释放内存. 即使我们全部按照上一篇文章中给出的编程建议来去编写程序,还是会很有可能出现内存泄露 或其它类型的内存问题.

你不知道的Eclipse的用法:使用MAT分析Android的内存

- - CSDN博客研发管理推荐文章
如果使用DDMS确实发现了我们程序中存在内存泄露,那如何定位到具体出现问题的代码片段,最终找到问题所在呢. 如果从头到尾分析代码逻辑,那肯定会把人逼疯,特别是在维护别人写的代码的时候. 这里介绍一个极好的内存分析工具Memory Analyzer Tool(MAT). 在Eclipse中安装和使用MAT步骤 .

Android 性能优化之使用MAT分析内存泄露问题

- - CSDN博客推荐文章
转载请注明本文出自xiaanming的博客( http://blog.csdn.net/xiaanming/article/details/42396507),请尊重他人的辛勤劳动成果,谢谢. 内存泄露就像一个定时炸弹,随时都有可能使我们的应用程序崩溃掉,所以作为一名Android开发人员,还是需要有分析内存泄露的能力,说道这里我们还是要说下什么是内存泄露,内存泄露是指有个引用指向一个不再被使用的对象,导致该对象不会被垃圾回收器回收.

android 内存清理

- - CSDN博客推荐文章
这两天在搞个内存清理小插件,网上很少这方面资料,之前找过清理缓存的例子测试,结果不成功. 后来自己思考该怎么清理内存,于是成功搞出来了. 这两个方法是网上拷别人的,分别用来得到可用内存和内存总数,这样我们就可用得到了已用内存数. // 获取android当前可用内存大小. //mi.availMem; 当前系统的可用内存.

Android内存管理

- - CSDN博客推荐文章
首先Android内存管理机制相当复杂,想要讲清楚比较困难;其次对于绝大多数用户来说,只关心内存够不够用,至于内存如何管理的这种技术细节,不是用户需要去考虑的,写这样一个专题有没有意义. 毕竟我们是用手机,不是来研究手机的. 最后的顾虑是这个专题会不会太技术化了,绝大部分用户不会看或者说缺乏相应的背景.

android 内存优化

- - CSDN博客推荐文章
      内存泄漏引发很多问题:.      1:程序卡顿,响应速度慢(内存占用高时JVM 虚拟机会频繁出发GC).      2:莫名其妙消失. ANDROID 内存面临的问题.   1: 有限的堆内存,原始只有16M.   2:内存大小消耗等根据设备,操作系统等级,尺寸的不同而不同.   4:支持后台多任务处理.

Android内存小谈 - Birdmafly

- - 博客园_首页
先来想一个问题:如果一个应用内存占用超过了16M,而手机系统分配的最大内存只有15M,那么会有什么问题. 没错,内存溢出,外在表现是手机卡死或者程序崩掉. 当然,Android有一套自己的垃圾回收机制,当一些被引用的对象不再使用的时候就当做“垃圾”来处理,用GC(垃圾处理器)释放掉被占用的那部分内存.

Android 解析内存泄漏

- - CSDN博客移动开发推荐文章
1、引用没释放造成的内存泄露.        1.1、注册没取消造成的内存泄露.        这种 Android的内存泄露比纯 Java的内存泄露还要严重,因为其他一些Android程序可能引用我们的Anroid程序的对象(比如注册机制). 即使我们的Android程序已经结束了,但是别的引用程序仍然还有对我们的Android程序的某个对象的引用,泄露的内存依然不能被垃圾回收.

Android内存管理之道

- - CSDN博客移动开发推荐文章
相信一步步走过来的Android从业者,每个人都会遇到OOM的情况. 如何避免和防范OOM的出现,对于每一个程序员来说确实是一门必不可少的能力. 今天我们就谈谈在Android平台下内存的管理之道,开始今天的主题之前,先再次回顾两个概念. 内存泄漏:对象在内存heap堆中中分配的空间,当不再使用或没有引用指向的情况下,仍不能被GC正常回收的情况.

Android:管理应用内存

- - CSDN博客推荐文章
所有内容均来源于官方文档 https://developer.android.com/training/articles/memory.html. only way to completely release memory from your app is to release object references you may be holding, making the memory available to the garbage collector.