内存泄漏解析

本篇对于Android中内存泄漏进行解析。

前言

相对于C/C++的内存泄漏是new出来的对象没有delete,而java中内存泄漏是new出来的对象放在Heap上无法被GC回收,所以对于内存泄漏,在开发过程中不可轻易忽视。

杂谈

首先谈谈Java的内存分配:

  1. 静态存储区: 编译时就分配好,在程序整个运行期间都存在,主要存放静态数据和常量;
  2. 栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存;
  3. 堆区:通常用来存放new出来的对象,由java垃圾回收期回收。

对于java垃圾回收机制回收不同引用类型的介绍:

  1. 强引用(StrongRefrrence): JVM宁可抛出OOM,也不会让GC回收具有强引用的对象;
  2. 软引用(SoftReference):只有在内存空间不足时,才会被回收的对象;
  3. 弱引用(WeakReference):在GC时,一旦发现只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;
  4. 虚引用(PhantomReference):任何时候都可以被GC回收。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

总结为以下:

级别 回收时机 用途 生存时间
从来不会 对象的一般状态 JVM停止运行时终止
在内存不足时 联合ReferenceQueue构造有效期短/占内存大/生命周期长的对象的二级高速缓冲器(内存不足才清空) 内存不足时终止
在垃圾回收时 联合ReferenceQueue构造有效期短/占内存大/生命周期长的对象的一级高速缓冲器(系统发生gc则清空) gc运行后终止
在垃圾回收时 联合ReferenceQueue来跟踪对象被垃圾回收器回收的活动 gc运行后终止

内存泄漏解析

1.持有Context

context是最容易忽视的,很多情况下我们可能随意传递context,例如给一个类传递context的时候经常用Activity,但该类持有对Activity的全部引用,当Activity关闭的时候因为被其他类持有,而导致无法正常被回收,从而导致内存泄漏。

解决方案:

在给类传递context的时候使用Application对象,避免依赖activity的生命周期,但谨慎对context使用static关键字。

2.Handler

Handler是最容易造成内存泄漏的,如果Handler中有延迟的任务或是等在执行的队列过长,由于消息队列持有对Handler的引用,而Handler又持有actvity的隐式引用,这个引用会保持到消息得到处理,而导致activity无法被垃圾回收器进行回收,而导致内存泄漏。

解决方案:

  1. 把Handler放到单独的类中,或者使用静态的内部类避免泄漏
  2. 如果想要在Handler内部去调用Activity中的资源,可以在Handler中使用弱引用的方式指向所在的Activity,使用static+WeakReference的方式断开handler与activity的关系

3.单例模式

在使用单例模式的时候如果使用不当也是会造成内存泄漏的,因为单例膜撕的静态特征使得单例模式的生命周期和应用一样的长,这说明了当一个对象不需要使用了,而单例对象还存在该对象的引用,那么这个对象就不能正常的被回收,导致内存泄漏。

解决方案:

在构建单例模式时,我们经常会传入Activity的context,当Activity退出之后,单例对象还持有他的引用,所以为了避免传Activity的context,在单例中通过传入的context获取到全局的上下文对象,而不适用Activity的Context就解决了这个问题。

4.非静态内部类创建静态实例

在非静态的内部类默认会持有外部类的引用,而我们又使用非静态内部类创建了一个静态的实例,该静态实例的声明周期和应用一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity不能正常回收。

解决方案:

  1. 将内部类修改成静态的,这样它对外部类就没有引用
  2. 将该对象抽取出来封装成一个单例。

5.线程

我们在使用线程时,一般都使用匿名内部类,而匿名内部类会对外部类持有默认的引用,当Activity关闭之后如果现成中的任务还没有执行完毕,就会导致Activity不能正常回收,造成内存泄漏。

解决方案:

创建一个静态的类,实现Runnable方法,在使用的时候实例化。

6.资源未关闭

对于使用了BroadcastReceiver、ContentObserver、File、Cursor、Stream、Bitmap等资源,应该在Activity销毁时及时关闭或者注销掉,否则这些资源不会被回收,造成内存泄漏。

7.监听器未注销

在很多地方需要register的监听器,要确保及时unregister监听器。

8.WebView

webView是个坑,很难驾驭,如果你在webView有js连调中有线程处理,不再需要使用webView的时候,应该调用它的destory()销毁,释放其占用的内存,否则其占用的内存长期也不能回收,从而造成内存泄漏。

9.集合容器

我们通常会把一些对象的引用加入到集合容器(比如ArrayList)中,当我们不再需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。

解决方案:

所以在退出程序之前,将集合里面的东西clear,然后置为null,再退出程序。

内存泄漏检测工具

对于检测工具,我推荐两种:

  1. MAT(Memory Analyzer Tool),具体使用方法网上很多。
  2. LeakCanary,这是我近2年一直在用的一个检测库,配置简单,抓捕率高,但某些机型上有些bug,所以开发阶段可以用,发版建议关闭。

总结

  • 构造Adapter时,没有使用缓存的 convertView
  • Bitmap对象不在使用时调用recycle()释放内存
  • Context使用不当造成内存泄露:不要对一个Activity Context保持长生命周期的引用。尽量在一切可以使用应用ApplicationContext代替Context的地方进行替换。
  • 非静态内部类的静态实例容易造成内存泄漏:即一个类中如果你不能够控制它其中内部类的生命周期(譬如Activity中的一些特殊Handler等),则尽量使用静态类和弱引用来处理(譬如ViewRoot的实现)。
  • 警惕线程未终止造成的内存泄露;譬如在Activity中关联了一个生命周期超过Activity的Thread,在退出Activity时切记结束线程。一个典型的例子就是HandlerThread的run方法是一个死循环,它不会自己结束,线程的生命周期超过了Activity生命周期,我们必须手动在Activity的销毁方法中中调运thread.getLooper().quit();才不会泄露。
  • 对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。
  • 创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等。
  • 不要在执行频率很高的方法或者循环中创建对象(比如onmeasure),可以使用HashTable等创建一组对象容器从容器中取那些对象,而不用每次new与释放。
  • 避免代码设计模式的错误造成内存泄露;譬如循环引用,A持有B,B持有C,C持有A,这样的设计谁都得不到释放。

参考

  1. Android内存泄漏解决方案(OOM)
  2. 内存泄漏全解析,从此拒绝ANR,让OOM远离你的身边,跟内存泄漏say byebye
坚持原创技术分享,您的支持将鼓励我继续创作!