http://blog.csdn.net/guolin_blog/article/details/42238633
问题说明
由于 Android 是为移动设备开发的操作系统, 我们在开发应用程序的时候应当始终把内存问题充分考虑在内. 虽然 Android 系统拥有垃圾自动回收机制, 但这并不意味着我们就可以完全忽略何时去分配或释放内存. 即使我们在写程序的时候, 会去注意这个问题, 还是会很有可能出现内存泄露或其它类型的内存问题. 所以, 唯一能够解决问题的办法, 就是尝试去分析应用程序的内存使用情况.
答题技巧
虽说现在的手机内存都已经非常大了, 但是我们大家都知道, 系统是不可能将所有的内存都分配给我们的应用程序的. 没错, 每个程序都会有可使用的内存上限, 这被称为堆大小(Heap Size). 不同的手机, 堆大小也不尽相同, 随着现在硬件设备不断提高, 堆大小也已经由 Nexus One 时的 32MB, 变成了 Nexus 5 时的 192MB. 如果大家想要知道自己手机的堆大小是多少, 可以调用如下代码:
- [java] view plain http://blog.csdn.net/guolin_blog/article/details/42238633 copy http://blog.csdn.net/guolin_blog/article/details/42238633 print http://blog.csdn.net/guolin_blog/article/details/42238633 ? http://blog.csdn.net/guolin_blog/article/details/42238633
- ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
- int heapSize = manager.getMemoryClass();
- ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
- int heapSize = manager.getMemoryClass();
结果是以 MB 为单位进行返回的, 我们在开发应用程序时所使用的内存不能超出这个限制, 否则就会出现 OutOfMemoryError.
如果我们想要更加清楚地实时知晓当前应用程序的内存使用情况, 我们需要通过 DDMS 中提供的工具来实现. 打开 DDMS 界面, 在左侧面板中选择你要观察的应用程序进程, 然后点击 Update Heap 按钮, 接着在右侧面板中点击 Heap 标签, 之后不停地点击 Cause GC 按钮来实时地观察应用程序内存的使用情况即可, 如下图所示:
接着继续操作我们的应用程序, 然后继续点击 Cause GC 按钮, 如果你发现反复操作某一功能会导致应用程序内存持续增高而不会下降的话, 那么就说明这里很有可能发生内存泄漏了.
大家需要知道的是, Android 中的垃圾回收机制并不能防止内存泄漏的出现, 导致内存泄漏最主要的原因就是某些长存对象持有了一些其它应该被回收的对象的引用, 导致垃圾回收器无法去回收掉这些对象, 那也就出现内存泄漏了. 比如说像 Activity 这样的系统组件, 它又会包含很多的控件甚至是图片, 如果它无法被垃圾回收器回收掉的话, 那就算是比较严重的内存泄漏情况了.
下面我们来模拟一种 Activity 内存泄漏的场景, 内部类相信大家都有用过, 如果我们在一个类中又定义了一个非静态的内部类, 那么这个内部类就会持有外部类的引用, 如下所示:
- [java] view plain http://blog.csdn.net/guolin_blog/article/details/42238633 copy http://blog.csdn.net/guolin_blog/article/details/42238633 print http://blog.csdn.net/guolin_blog/article/details/42238633 ? http://blog.csdn.net/guolin_blog/article/details/42238633
- public class MainActivity extends ActionBarActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- LeakClass leakClass = new LeakClass();
- }
- class LeakClass {
- }
- ......
- }
- public class MainActivity extends ActionBarActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- LeakClass leakClass = new LeakClass();
- }
- class LeakClass {
- } ...... }
目前来看, 代码还是没有问题的, 因为虽然 LeakClass 这个内部类持有 MainActivity 的引用, 但是只要它的存活时间不会长于 MainActivity, 就不会阻止 MainActivity 被垃圾回收器回收. 那么现在我们来将代码进行如下修改:
- [java] view plain http://blog.csdn.net/guolin_blog/article/details/42238633 copy http://blog.csdn.net/guolin_blog/article/details/42238633 print http://blog.csdn.net/guolin_blog/article/details/42238633 ? http://blog.csdn.net/guolin_blog/article/details/42238633
- public class MainActivity extends ActionBarActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- LeakClass leakClass = new LeakClass();
- leakClass.start();
- }
- class LeakClass extends Thread {
- @Override
- public void run() {
- while (true) {
- try {
- Thread.sleep(60 * 60 * 1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- ......
- }
- public class MainActivity extends ActionBarActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- LeakClass leakClass = new LeakClass();
- leakClass.start();
- }
- class LeakClass extends Thread {
- @Override
- public void run() {
- while (true) {
- try {
- Thread.sleep(60 * 60 * 1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- } } ...... }
这下就有点不太一样了, 我们让 LeakClass 继承自 Thread, 并且重写了 run()方法, 然后在 MainActivity 的 onCreate()方法中去启动 LeakClass 这个线程. 而 LeakClass 的 run()方法中运行了一个死循环, 也就是说这个线程永远都不会执行结束, 那么 LeakClass 这个对象就一直不能得到释放, 并且它持有的 MainActivity 也将无法得到释放, 那么内存泄露就出现了.
现在我们可以将程序运行起来, 然后不断地旋转手机让程序在横屏和竖屏之间切换, 因为每切换一次 Activity 都会经历一个重新创建的过程, 而前面创建的 Activity 又无法得到回收, 那么长时间操作下我们的应用程序所占用的内存就会越来越高, 最终出现 OutOfMemoryError.
下面我贴出一张不断切换横竖屏时 GC 日志打印的结果图, 如下所示:
可以看到, 应用程序所占用的内存是在不断上升的. 最可怕的是, 这些内存一旦升上去了就永远不会再降下来, 直到程序崩溃为止, 因为这部分泄露的内存一直都无法被垃圾回收器回收掉.
那么通过上面学习 DDMS 工具这种方式, 现在我们已经可以比较轻松地发现应用程序中是否存在内存泄露的现象了. 但是如果真的出现了内存泄露, 我们应该怎么定位到具体是哪里出的问题呢? 这就需要借助一个内存分析工具了, 叫做 Memory Analyzer Tool(MAT). 这个工具分为 Eclipse 插件版和独立版两种, 如果你是使用 Eclipse 开发的, 那么可以使用插件版 MAT, 非常方便. 如果你是使用 Android Studio 开发的, 那么就只能使用独立版的 MAT 了.
那么接下来我们开始学习如何去分析内存泄露的原因, 首先还是进入到 DDMS 界面, 然后在左侧面板选中我们要观察的应用程序进程, 接着点击 Dump HPROF file 按钮, 如下图所示:
点击这个按钮之后需要等待一段时间, 然后会生成一个 HPROF 文件, 这个文件记录着我们应用程序内部的所有数据. 但是目前 MAT 还是无法打开这个文件的, 我们还需要将这个 HPROF 文件从 Dalvik 格式转换成 J2SE 格式, 使用 hprof-conv 命令就可以完成转换工作, 如下所示:
- [plain] view plain http://blog.csdn.net/guolin_blog/article/details/42238633 copy http://blog.csdn.net/guolin_blog/article/details/42238633 print http://blog.csdn.net/guolin_blog/article/details/42238633 ? http://blog.csdn.net/guolin_blog/article/details/42238633
- hprof-conv dump.hprof converted-dump.hprof // 直接进入 hprof-conv 坐在目录执行该命令
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[], 最终也是能找到内存泄漏所在的.
好了, 这大概就是 MAT 工具最常用的一些用法了, 当然这里还要提醒大家一句, 工具是死的, 人是活的, MAT 也没有办法保证一定可以将内存泄漏的原因找出来, 还是需要我们对程序的代码有足够多的了解, 知道有哪些对象是存活的, 以及它们存活的原因, 然后再结合 MAT 给出的数据来进行具体的分析, 这样才有可能把一些隐藏得很深的问题原因给找出来.
来源: http://www.bubuko.com/infodetail-2551944.html