内存管理之内存泄漏

背景
【内存管理之内存泄漏】内存泄漏可能是我们最常遇见的异常类型。
所谓内存泄漏,即我们对一个不会再被使用的对象保持着强引用。这时,GC(垃圾回收器)是不会回收它们的,它们长期占用着有限的堆内存空间(这里的内存泄漏仅仅就堆内存说明,但是内存泄漏显然不仅仅是由堆内存所造成)。正所谓冰冻三尺非一日之寒,这种只增不减的消耗,最终堆内存空间会被这些垃圾占领,最后,崩溃。
内存中可回收的对象以及回收算法
首先,我们来看看什么样的对象是可以被回收的。
标记一个对象是否可以被回收的常用方法,通常有引用计数和可到达性分析。
引用计数,顾名思义,就是对一个对象的引用进行计数记录。如果一个对象的引用计数小于等于零,就是可以被回收的,但是它解决不了相互引用的问题。
可到达性分析,使用图论的概念,通过一系列的 GC Roots 判断一个对象是否是可以到达的,如果是无法到达的孤岛,则可以进行回收。
回收算法则依赖于虚拟机的具体实现。通常算法有 标记清理算法、 复制算法(需要两块相同大小的内存)、标记压缩算法(对前两种算法的综合优化)、分代算法(新生代使用复制算法;老年代使用标记清理算法或者标记压缩算法)。
对于大部分 Dalvik 虚拟机,使用的是标记清理算法。该算法会经历 标记 和 清理 两个阶段。在标记阶段,通过可达性分析,标记存活的对象;在清理阶段,清理未被标记的对象。
这种算法最大的缺陷在于容易造成内存碎片。
对于 ART 虚拟机,对内存回收进行了优化,分为前台 GC 和后台 GC 。应用运行在前台时,考虑到响应速度,使用标记清理算法;应用运行在后台时,使用后台 GC ,执行标记压缩算法,有效降低堆内存碎片。(当然相比 Dalvik,ART还有很有其他优势)
无论 Android 使用哪种虚拟机,标记清理算法的标记阶段,判断一个对象是否存活,使用的是可达性分析。我们要确保,对于一个不再使用的对象,不要直接或者间接持有对它的强引用。
下面,我会描述一些经常会被我们忽视的情况,导致内存泄漏。
内部类
有时候,为了方便和逻辑的清晰,会选择使用内部类,尤其是匿名内部类。
内部类分为静态内部类和非静态内部类,可以在类中定义内部类,也可以在方法中定义(匿名内部类)。
静态内部类不会持有外部类的引用;但是非静态的内部类就不一样了,它在存在期间会一直保持着对外部类的引用。这样,问题就来了,如果外部类已经没有用了,但是内部类却一直存在,那么外部类对象资源就无法得到释放。
最常见的就是 Runnable 和 Handler 类的使用。
其中,Runnable 用于定义线程。如下所示:

new Thread(()->{ while(true){ if(isOnline){ display1(); } else{ display2(); } Thread.sleep(3*1000); } }).start();

如果不在销毁外部类之前,关闭这个内部定义的线程,只要线程一直执行,它对外部类的引用就不会得到释放,外部对象也就不可能被 GC 回收。假设该线程是定义在一个 Activity 或者 Service 组件中,组件已经被 Destroy 掉了,但是无法被回收,这对性能的影响是很大的。
类似的,Handler 也存在这样的情况。Handler 的实际生命周期也可能会大于外部类的。发送消息的时间可能是在将来:
handler.postDelayed(new Runnable() { @Override public void run() {} },1000*60*5);

以上,延时 5 分钟发送消息,在这 5 分钟内,如果外部对象被销毁,但是它所拥有的资源是不会被销毁的。
即使不是这种推迟发送的情况,如果接受消息的线程需要处理大量的 Message 或者 Callback,那么,handle部分同样会被延时执行。
解决方案就是使用弱引用,或者在外部类销毁的时候,remove 掉所有 message 或者 callback。
class MyHandler extends Handler{ private WeakReference activity; public MyHandler(Activity activity){ super(); this.activity = new WeakReference(activity); }@Override public void handleMessage(Message msg) { if(activity.get() != null){ doSomething(); } } }

handler.removeCallbacksAndMessages(null);

比较隐藏一点的,可能就是属性动画了,定义好了属性动画之后,通常我们会设置监听匿名类。属性动画在执行期间,会一直存活,特别是 setDuration 很长时间,如果外部对象并没有销毁,通常得不到资源释放。你需要注意在不销毁它的时候,cancel 掉动画。
总之,切记,如果对象内属性(变量)的生命周期比对象长,而属性(变量)本身又持有该对象的引用,这时要注意在对象被销毁的时候,结束属性(变量)的生命周期。
View及其子类
通常,我们会不假思索地通过 findViewById 来给一个 View 赋值,但是忽略了一个事实:那就是所有的 View 的创建,事实上都依赖于一个 Context。也就是说 View 和 Context 是一种强绑定的关系。
@BindingAdapter({"imgUrl"}) public static void loadImage(ImageView view, String imageUrl) { RequestCreator creator = PicassoUtils.getRequestCreator(imageUrl); creator.fetch(new Callback() { @Override public void onSuccess() {}@Override public void onError() { try { Thread.sleep(5*1000); } catch (InterruptedException e) { e.printStackTrace(); } loadImage(view,imageUrl); } }); creator.into(view); }

上述代码块想要完成的逻辑,就是希望在图片加载异常时,能够重新加载图片,同时,静态方法决定了它不会持有外部对象的引用。
但是,问题就在于其中的 ImageView ,它的创建是依赖于当前 Activity,如果当前 Activity 已经被销毁,由于 ImageView 还一直持有 Activity 的引用,Activity 将不能得到回收,而 Activity 中有持有者大量对象的引用,这时的内存泄漏是很严重的。
如果是个 ApplicationContext 还好(事实上, View 的创建不一定是要依赖于一个 Activity,它依赖的是一个 Context,ApplicationContext 也可以,主要还是看场景需求)。这也是我们通常在做某些初始化时,为什么尽量使用ApplicationConetxt 的原因。
所以,同样需要注意你所使用的 View 的生命周期,不要让它的生命长度大于它所依赖的 Activity(如果它确实依赖一个Activity 的 Context 的话)。
静态成员
我们的知道,静态的资源是无法被回收的,即使是可回收的(不可到达)。 通常,我们也会小心翼翼的使用它,但是有一些细节,很容易被忽略。
如果将 View 设置为静态成员,那么,它所持有的 context 对象资源也将不会被回收。
所以,应当注意,谨慎使用静态成员,如果使用了静态成员,注意它是否持有非静态对象引用。
关闭资源
事实上,关于这方面容易造成内存溢出的问题,多说就是老调重弹了。当不需使用它的时候,注意关闭它,包括 IO ,数据库等。如果不关闭它们,会有一些系统资源得不到释放,比如说缓冲对象。
所以,你要确保,每一种被打开的资源,不论发生什么情况,都会得到关闭。
这里指的注意的是 try catch finally 对。它们之间是的操作对象实际上是一种异常。发现异常,catch 捕获处理异常,在 finally 中做一些清理工作。
如果是这样
InputStream mInputStream = null; OutputStream mOutputStream = null; try { mInputStream = new FileInputStream(inputPath); mOutputStream = new FileOutputStream(outputPath); //处理流 } catch(FileNotFoundException e) { //处理异常 } finally { //关闭输入流 if (mInputStream != null) { try { mInputStream.close(); } catch(IOException ioex) {} } //如果输入流在关闭过程中出现异常,不做捕获处理,将异常这里输出流的关闭 if (mOutputStream != null) { try { mOutputStream.close(); } catch(IOException e) { //异常处理 } } }

上面的处理方式,在格式上来看确实不怎么美观,但是我们必须这么做。如果你不喜欢,可以自己把这个过程封装成一个 IO 工具类。
小结
在做内存优化的时候,总结了一下自己使用的一些方法,希望能够能够对内存泄漏的理解能够更加系统。
事实上,我们确实不用操心如果一个对象所占用的空间如何被释放,但是,我们还是要关注它们的生命周期,确保它们能够被释放,这个确定权,其实在我们每个开发者的手中。
参考链接
Android GC 那点事
图解Java 垃圾回收机制
JVM 内存模型概述

    推荐阅读