Android 内存泄露总结
TangelaPerr
8年前
<h2><strong>Java 中的内存分配</strong></h2> <p>主要是分三块:</p> <ul> <li> <p>静态储存区:编译时就分配好,在程序整个运行期间都存在。它主要存放静态数据和常量。</p> </li> <li> <p>栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存。</p> </li> <li> <p>堆区:通常存放 new 出来的对象。由 Java 垃圾回收器回收。</p> </li> </ul> <h2><strong>栈与堆的区别</strong></h2> <p>栈内存用来存放局部变量和函数参数等。它是先进后出的队列,进出一一对应,不产生碎片,运行效率稳定高。当超过变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。</p> <p>堆内存用于存放对象实例。在堆中分配的内存,将由Java垃圾回收器来自动管理。在堆内存中频繁的 new/delete 会造成大量内存碎片,使程序效率降低。</p> <p>对于非静态变量的储存位置,我们可以粗暴的认为:</p> <ul> <li> <p>局部变量位于栈中(其中引用变量指向的对象实体存在于堆中)。</p> </li> <li> <p>成员变量位于堆中。因为它们属于类,该类最终被new成对象,并作为一个整体储存在堆中。</p> </li> </ul> <h2><strong>四种引用类型的介绍</strong></h2> <p>GC 释放对象的根本原则是该对象不再被引用(强引用)。那么什么是强引用呢?</p> <p><strong>强引用(Strong Reference)</strong></p> <p>我们平常用的最多的就是强引用,如下:</p> <pre> <code class="language-java">IPhotos iPhotos = new IPhotos();</code></pre> <p>JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象。强引用不使用时,可以通过 obj = null 来显式的设置该对象的所有引用为 null,这样就可以回收该对象了。至于什么时候回收,取决于 GC 的算法,这里不做深究。</p> <p><strong>软引用(Soft Reference)</strong></p> <pre> <code class="language-java">SoftReference<String> softReference = new SoftReference<>(str);</code></pre> <p>如果一个对象只具有软引用,那么在内存空间足够时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被使用。</p> <p>软引用曾经常被用来作图片缓存,然而谷歌现在推荐用 LruCache 替代,因为 LRU 更高效。</p> <p>In the past, a popular memory cache implementation was a SoftReference</p> <p>or WeakReference bitmap cache, however this is not recommended.</p> <p>Starting from Android 2.3 (API Level 9) the garbage collector is more</p> <p>aggressive with collecting soft/weak references which makes them</p> <p>fairly ineffective. In addition, prior to Android 3.0 (API Level 11),</p> <p>the backing data of a bitmap was stored in native memory which is not</p> <p>released in a predictable manner, potentially causing an application</p> <p>原文</p> <p>大致意思是:因为在 Android 2.3 以后,GC 会很频繁,导致释放软引用的频率也很高,这样会降低它的使用效率。并且 3.0 以前 Bitmap 是存放在 Native Memory 中,它的释放是不受 GC 控制的,所以使用软引用缓存 Bitmap 可能会造成 OOM。</p> <p><strong>弱引用(Weak Reference)</strong></p> <pre> <code class="language-java">WeakReference<String> weakReference = new WeakReference<>(str);</code></pre> <p>与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。因为在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象- -。</p> <p><strong>虚引用(PhantomReference)</strong></p> <p>顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期,也无法通过虚引用获得对象实例。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。</p> <h2><strong>Android的垃圾回收机制简介</strong></h2> <p>Android 系统里面有一个 Generational Heap Memory 模型,系统会根据内存中不同的内存数据类型分别执行不同的 GC 操作。</p> <p>该模型分为三个区:</p> <ul> <li> <p>Young Generation</p> <ol> <li> <p>eden</p> </li> <li> <p>Survivor Space</p> <ol> <li> <p>S0</p> </li> <li> <p>S1</p> </li> </ol> </li> </ol> </li> <li> <p>Old Generation</p> </li> <li> <p>Permanent Generation</p> </li> </ul> <h2><strong>Young Generation</strong></h2> <p>大多数 new 出来的对象都放到 eden 区,当 eden 区填满时,执行 Minor GC(轻量级GC),然后存活下来的对象被转移到 Survivor 区(有 S0,S1 两个)。 Minor GC 也会检查 Survivor 区的对象,并把它们转移到另一个 Survivor 区,这样就总会有一个 Survivor 区是空的。</p> <h2><strong>Old Generation</strong></h2> <p>存放长期存活下来的对象(经过多次 Minor GC 后仍然存活下来的对象) Old Generation 区满了以后,执行 Major GC(大型 GC)。</p> <p>在Android 2.2 之前,执行 GC 时,应用的线程会被暂停,2.3 开始添加了并发垃圾回收机制。</p> <h2><strong>Permanent Generation</strong></h2> <p>存放方法区。一般存放:</p> <ul> <li> <p>要加载的类的信息</p> </li> <li> <p>静态变量</p> </li> <li> <p>final常量</p> </li> <li> <p>属性、方法信息</p> </li> </ul> <h2><strong>60 FPS</strong></h2> <p>这里简单的介绍一下帧率的概念,以便于理解为什么大量的 GC 容易引起卡顿。</p> <p>App 开发时,一般追求界面的帧率达到60 FPS(60 帧/秒),那这个 FPS 是什么概念呢?</p> <ul> <li> <p>10-12 FPS 时可以感受到动画的效果;</p> </li> <li> <p>24 FPS,可以感受到平滑连贯的动画效果,电影常用帧率(不追求 60 FPS 是节省成本);</p> </li> <li> <p>60 FPS,达到最流畅的效果,对于更高的FPS,大脑已经难以察觉区别。</p> </li> </ul> <p>Android 每隔 16 ms发出 VSYNC 信号,触发对 UI 的渲染(即每 16 ms绘制一帧),如果整个过程保持在 16 ms以内,那么就会达到 60 FPS 的流畅画面。超过了 16 ms就会造成卡顿。那么如果在 UI 渲染时发生了大量 GC,或者 GC 耗时太长,那么就可能导致绘制过程超过 16 ms从而造成卡顿(FPS 下降、掉帧等),而我们大脑对于掉帧的情况十分敏锐,因此如果没有做好内存管理,将会给用户带来非常不好的体验。</p> <p>再介绍一下内存抖动的概念,本文后面可能会用到这个概念。</p> <p><strong>内存抖动</strong></p> <p>短时间内大量 new 对象,达到 Young Generation 的阈值后触发GC,导致刚 new 出来的对象又被回收。此现象会影响帧率,造成卡顿。</p> <p>内存抖动在 Android 提供的 Memory Monitor 中大概表现为这样:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/7579c9ea9adb50fcc7b91b3cc3afe61b.png"></p> <h2><strong>Android中常见的内存泄露及解决方案</strong></h2> <p><strong>集合类泄露</strong></p> <p>如果某个集合是全局性的变量(比如 static 修饰),集合内直接存放一些占用大量内存的对象(而不是通过弱引用存放),那么随着集合 size 的增大,会导致内存占用不断上升,而在 Activity 等销毁时,集合中的这些对象无法被回收,导致内存泄露。比如我们喜欢通过静态 HashMap 做一些缓存之类的事,这种情况要小心,集合内对象建议采用弱引用的方式存取,并考虑在不需要的时候手动释放。</p> <p><strong>单例造成的内存泄露</strong></p> <p>单例的静态特性导致其生命周期同应用一样长。</p> <p>有时创建单例时如果我们需要Context对象,如果传入的是Application的Context那么不会有问题。如果传入的是Activity的Context对象,那么当Activity生命周期结束时,该Activity的引用依然被单例持有,所以不会被回收,而单例的生命周期又是跟应用一样长,所以这就造成了内存泄露。</p> <p>解决办法一:在创建单例的构造中不直接用传进来的context,而是通过这个context获取Application的Context。代码如下:</p> <pre> <code class="language-java">public class AppManager { private static AppManager instance; private Context context; private AppManager(Context context) { this.context = context.getApplicationContext();// 使用Application 的context } public static AppManager getInstance(Context context) { if (instance != null) { instance = new AppManager(context); } return instance; } }</code></pre> <p>第二种解决方案:在构造单例时不需要传入 context,直接在我们的 Application 中写一个静态方法,方法内通过 getApplicationContext 返回 context,然后在单例中直接调用这个静态方法获取 context。</p> <p><strong>非静态内部类造成的内存泄露</strong></p> <p>在 Java 中,非静态内部类(包括匿名内部类,比如 Handler, Runnable匿名内部类最容易导致内存泄露)会持有外部类对象的强引用(如 Activity),而静态的内部类则不会引用外部类对象。</p> <p>非静态内部类或匿名类因为持有外部类的引用,所以可以访问外部类的资源属性成员变量等;静态内部类不行。</p> <p>因为普通内部类或匿名类依赖外部类,所以必须先创建外部类,再创建普通内部类或匿名类;而静态内部类随时都可以在其他外部类中创建。</p> <p>Handler内存泄露可以关注我的另一篇专门针对Handler内存泄露的文章:链接</p> <p><strong>WebView 的泄漏</strong></p> <p>Android 中的 WebView 存在很大的兼容性问题,有些 WebView 甚至存在内存泄露的问题。所以通常根治这个问题的办法是为 WebView 开启另外一个进程,通过 AIDL 与主进程进行通信, WebView 所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。</p> <p><strong>AlertDialog 造成的内存泄露</strong></p> <pre> <code class="language-java">new AlertDialog.Builder(this) .setPositiveButton("Baguette", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { MainActivity.this.makeBread(); } }).show();</code></pre> <p>DialogInterface.OnClickListener 的匿名实现类持有了 MainActivity 的引用;</p> <p>而在 AlertDialog 的实现中,OnClickListener 类将被包装在一个 Message 对象中(具体可以看 AlertController 类的 setButton 方法),而且这个 Message 会在其内部被复制一份(AlertController 类的 mButtonHandler 中可以看到),两份 Message 中只有一个被 recycle,另一个(OnClickListener 的成员变量引用的 Message 对象)将会泄露!</p> <p>解决办法:</p> <ul> <li> <p>Android 5.0 以上不存在此问题;</p> </li> <li> <p>Message 对象的泄漏无法避免,但是如果仅仅是一个空的 Message 对象,将被放入对象池作为后用,是没有问题的;</p> </li> <li> <p>让 DialogInterface.OnClickListener 对象不持有外部类的强引用,如用 static 类实现;</p> </li> <li> <p>在 Activity 退出前 dismiss dialog</p> </li> </ul> <p><strong>Drawable 引起的内存泄露</strong></p> <p>Android 在 4.0 以后已经解决了这个问题。这里可以跳过。</p> <p>当我们屏幕旋转时,默认会销毁掉当前的 Activity,然后创建一个新的 Activity 并保持之前的状态。在这个过程中,Android 系统会重新加载程序的UI视图和资源。假设我们有一个程序用到了一个很大的 Bitmap 图像,我们不想每次屏幕旋转时都重新加载这个 Bitmap 对象,最简单的办法就是将这个 Bitmap 对象使用 static 修饰。</p> <pre> <code class="language-java">private static Drawable sBackground; @Override protected void onCreate(Bundle state) { super.onCreate(state); TextView label = new TextView(this); label.setText("Leaks are bad"); if (sBackground == null) { sBackground = getDrawable(R.drawable.large_bitmap); } label.setBackgroundDrawable(sBackground); setContentView(label); }</code></pre> <p>但是上面的方法在屏幕旋转时有可能引起内存泄露,因为,当一个 Drawable 绑定到了 View 上,实际上这个 View 对象就会成为这个 Drawable 的一个 callback 成员变量,上面的例子中静态的 sBackground 持有 TextView 对象的引用,而 TextView 持有 Activity 的引用。当屏幕旋转时,Activity 无法被销毁,这样就产生了内存泄露问题。</p> <p>该问题主要产生在 4.0 以前,因为在 2.3.7 及以下版本 Drawable 的 setCallback 方法的实现是直接赋值,而从 4.0.1 开始,setCallback 采用了弱引用处理这个问题,避免了内存泄露问题。</p> <p><strong>资源未关闭造成的内存泄露</strong></p> <ul> <li> <p>BroadcastReceiver,ContentObserver 之类的没有解除注册</p> </li> <li> <p>Cursor,Stream 之类的没有 close</p> </li> <li> <p>无限循环的动画在 Activity 退出前没有停止</p> </li> <li> <p>一些其他的该 release 的没有 release,该 recycle 的没有 recycle… 等等。</p> </li> </ul> <h2><strong>总结</strong></h2> <p>我们不难发现,大多数问题都是 static 造成的!</p> <ul> <li> <p>在使用 static 时一定要小心,关注该 static 变量持有的引用情况。在必要情况下使用弱引用的方式来持有一些引用</p> </li> <li> <p>在使用非静态内部类时也要注意,毕竟它们持有外部类的引用。(使用 RxJava 的同学在 subscribe 时也要注意 unSubscribe)</p> </li> <li> <p>注意在生命周期结束时释放资源</p> </li> <li> <p>使用属性动画时,不用的时候请停止(尤其是循环播放的动画),不然会产生内存泄露(Activity 无法释放)(View 动画不会)</p> </li> </ul> <h2><strong>几种内存检测工具的介绍</strong></h2> <ul> <li> <p>Memory Monitor</p> </li> <li> <p>Allocation Tracker</p> </li> <li> <p>Heap Viewer</p> </li> <li> <p>LeakCanary</p> </li> </ul> <p><strong>Memory Monitor</strong></p> <p>位于 Android Monitor 中,该工具可以:</p> <ul> <li> <p>方便的显示内存使用和 GC 情况</p> </li> <li> <p>快速定位卡顿是否和 GC 有关</p> </li> <li> <p>快速定位 Crash 是否和内存占用过高有关</p> </li> <li> <p>快速定位潜在的内存泄露问题(内存占用一直在增长)</p> </li> <li> <p>但是不能准确的定位问题</p> </li> </ul> <p><strong>Allocation Tracker</strong></p> <p>该工具用途:</p> <ul> <li> <p>可以定位代码中分配的对象类型、大小、时间、线程、堆栈等信息</p> </li> <li> <p>可以定位内存抖动问题</p> </li> <li> <p>配合 Heap Viewer 定位内存泄露问题(可以找出来泄露的对象是在哪创建的等等)</p> </li> </ul> <p>使用方法:在 Memory Monitor 中有个 Start Allocation Tracking 按钮即可开始跟踪 在点击停止跟踪后会显示统计结果。</p> <p><strong>Heap Viewer</strong></p> <p>该工具用于:</p> <ul> <li> <p>显示内存快照信息</p> </li> <li> <p>每次 GC 后收集一次信息</p> </li> <li> <p>查找内存泄露的利器</p> </li> </ul> <p>使用方法: 在 Memory Monitor 中有个 Dump Java Heap 按钮,点击即可,在统计报告左上角选按 package 分类。配合 Memory Monitor 的 initiate GC(执行 GC)按钮,可检测内存泄露等情况。</p> <p><strong>LeakCanary</strong></p> <p>重要的事情说三遍:</p> <pre> <code class="language-java">for (int i = 0; i < 3; i++) { Log.e(TAG, "检测内存泄露的神器!"); }</code></pre> <p> </p> <p>来自:https://segmentfault.com/a/1190000006852540</p> <p> </p>