Android性能优化之内存泄漏
MildredJean
8年前
<p>在Android开发的过程中,经常需要注意内存泄漏问题,不然很容易导致OOM问题,或者因此引起频繁gc造成app卡顿。 下面这篇文章将分析内存泄漏的原因、Android内存管理的相关内容,并分享一些检测泄漏的方法和如何避免内存泄漏。</p> <h2>1. 内存泄漏的定义</h2> <p>Android是基于Java的,众所周知Java语言的内存管理是其一大特点, 不用像C语言那样处理对象的内存分配到回收的全部过程。在Java中我们只需要简单地新建对象就可以了, Java垃圾回收器会负责回收释放对象内存。 这么看的话,垃圾回收器会管理内存又怎么还会发生内存泄漏呢?</p> <p>其实Java中的内存泄漏的定义是: 对象不再被程序所使用, 但是由于这些对象被引用着导致GC(Garbage Collector)不能回收它们。</p> <p>下面这张图可以帮助我们更好地理解对象的状态,以及内存泄漏的情况</p> <p><img src="https://simg.open-open.com/show/5baf8269fd102e6bdd162ed295c865b4.jpg"></p> <p>左边未引用的对象是会被GC回收的,右边被引用的对象不会被GC回收,但是未使用的对象中除了未引用的对象,还包括已被引用的一部分对象,那么内存泄漏久发生这部分已被引用但未使用的对象。</p> <p>接下来还有一个疑问:未使用的对象被谁引用会让GC无法回收呢?</p> <p>现在主流的程序语言的主流实现中, 是通过可达性分析(Reachability Analysis)来判断对象是否存活的。这个算法的基本思路是:通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时,说明此对象不可用,可以被回收了。</p> <p>可以作为GC Roots的对象包括下面几种:</p> <ul> <li> <p>虚拟机栈中引用的对象, 一般是当前在使用中局部变量</p> </li> <li> <p>方法区中类静态属性引用的对象, 就是静态变量对应的对象</p> </li> <li> <p>方法区中常量引用的对象</p> </li> <li> <p>本地方法栈中JNI(即一般说的Native方法)引用的对象</p> </li> </ul> <p>MAT分析内存泄漏的时候,也是查看对象到GC Roots的引用链,来定位泄漏代码的位置。</p> <p>所以未使用的对象直接或间接地被GC Roots引用时会让GC无法回收,从而产生内存泄漏。</p> <h2>2. Android的内存管理</h2> <p>了解了Java的内存泄漏的起因,接下来大致了解Android中的内存管理机制。</p> <p>Google在Android的官网上有这样一篇文章,初步介绍了Android是如何管理应用的进程与内存分配:http://developer.android.com/training/articles/memory.html。 Android系统的Dalvik虚拟机扮演了常规的内存垃圾自动回收的角色,Android系统没有为内存提供交换区,它使用 <a href="/misc/goto?guid=4959725701683439653" rel="nofollow,noindex">paging</a> 与 <a href="/misc/goto?guid=4959725701769655871" rel="nofollow,noindex">memory-mapping(mmapping)</a> 的机制来管理内存,下面简要概述一些Android系统中重要的内存管理基础概念。</p> <p>分配与回收内存</p> <p>每一个进程的Dalvik heap都反映了使用内存的占用范围。这就是通常逻辑意义上提到的Dalvik Heap Size,它可以随着需要进行增长,但是增长行为会有一个系统为它设定的上限。</p> <p>逻辑上讲的Heap Size和实际物理意义上使用的内存大小是不对等的,Proportional Set Size(PSS)记录了应用程序自身占用以及和其他进程进行共享的内存。</p> <p>Android系统并不会对Heap中空闲内存区域做碎片整理。系统仅仅会在新的内存分配之前判断Heap的尾端剩余空间是否足够,如果空间不够会触发gc操作,从而腾出更多空闲的内存空间。在Android的高级系统版本里面针对Heap空间有一个Generational Heap Memory的模型,最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。系统会根据内存中不同的内存数据类型分别执行不同的gc操作。例如,刚分配到Young Generation区域的对象通常更容易被销毁回收,同时在Young Generation区域的gc操作速度会比Old Generation区域的gc操作速度更快。如下图所示:</p> <p><img src="https://simg.open-open.com/show/92dbefc340fc0efc9df7d149e9696095.png"> <img src="https://simg.open-open.com/show/4d88422f4213fb298267857de8fc6bb2.png"></p> <p>每一个Generation的内存区域都有固定的大小,随着新的对象陆续被分配到此区域,当这些对象总的大小快达到这一级别内存区域的阀值时,会触发GC的操作,以便腾出空间来存放其他新的对象。如下图所示:</p> <p><img src="https://simg.open-open.com/show/fdcbce08ceae58b61f34123f1f7d24b7.png"></p> <p>通常情况下,GC发生的时候,所有的线程都是会被暂停的。执行GC所占用的时间和它发生在哪一个Generation也有关系,Young Generation中的每次GC操作时间是最短的,Old Generation其次,Permanent Generation最长。执行时间的长短也和当前Generation中的对象数量有关,遍历树结构查找20000个对象比起遍历50个对象自然是要慢很多的。</p> <p>为什么通常情况下,GC发生的时候,所有的线程都会被暂停?</p> <p>因为每次GC的时候,需要先找到可作为GC Roots的对象,然后以此搜索引用链,这个过程需要在一致性的内存快照中进行。这个“一致性”表示在整个过程中不能出现对象引用关系不断变化的情况,所以需要暂停所有的执行线程。</p> <p>限制应用的内存</p> <p>为了整个Android系统的内存控制需要,Android系统为每一个应用程序都设置了一个硬性的Dalvik Heap Size最大限制阈值,这个阈值在不同的设备上会因为RAM大小不同而各有差异。如果你的应用占用内存空间已经接近这个阈值,此时再尝试分配内存的话,很容易引起OutOfMemoryError的错误。</p> <p>ActivityManager.getMemoryClass()可以用来查询当前应用的Heap Size阈值,这个方法会返回一个整数,表明你的应用的Heap Size阈值是多少Mb(megabates)。</p> <p>还有一个用adb命令查询的方法:</p> <pre> <code class="language-java">adb shell getprop dalvik.vm.heapgrowthlimit</code></pre> <h2>3. 检测与定位内存泄漏</h2> <p>(1)adb命令</p> <pre> <code class="language-java">adb shell dumpsys meminfo {package_name} </code></pre> <p>(2)Android Studio的Memory Monitor</p> <p>(3)LeakCanary</p> <p>(4)MAT</p> <p>在Android检查内存泄漏,主要搜索Activity、Fragment、View有没有泄漏。</p> <h2>4. 如何避免内存的总结</h2> <p>(1) 注意Activity的泄漏</p> <ul> <li>内部类引用导致Activity泄漏</li> </ul> <p>具体见 <a href="/misc/goto?guid=4959725701850759207" rel="nofollow,noindex">Android中由Handler和内部类引起的内存泄</a></p> <ul> <li>Activity Context被间接引用</li> </ul> <p>对于大部分非必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),我们都可以考虑使用Application Context而不是Activity的Context,这样可以避免不经意的Activity泄露</p> <p>(2) 注意静态变量和单例模式</p> <p>静态变量是作为GC Roots,在Android其生命周期基本和进程一样长,所以要非常静态变量引用其他生命周期的对象。虽然单例模式简单实用,提供了很多便利性,但是因为单例的生命周期和应用保持一致,使用不合理很容易出现持有对象的泄漏。</p> <p>(3) 注意容器中对象泄漏</p> <p>有时候,我们为了提高对象的复用性把某些对象放到缓存容器中,可是如果这些对象没有及时从容器中清除,也是有可能导致内存泄漏的。例如,针对2.3的系统,如果把drawable添加到缓存容器,因为drawable与View的强应用,很容易导致activity发生泄漏。而从4.0开始,就不存在这个问题。解决这个问题,需要对2.3系统上的缓存drawable做特殊封装,处理引用解绑的问题,避免泄漏的情况。</p> <p>(4) 注意监听器的注销</p> <p>(5) …</p> <p>(6) 及时关闭Cursor</p> <p>在程序中我们经常会进行查询数据库的操作,但时常会存在不小心使用Cursor之后没有及时关闭的情况。这些Cursor的泄露,反复多次出现的话会对内存管理产生很大的负面影响,我们需要谨记对Cursor对象的及时关闭。</p> <p> </p> <p>来自:http://johnnyshieh.github.io/android/2016/11/18/android-memory-leak/</p> <p> </p>