内存管理之内存泄漏

背景

内存泄漏可能是我们最常遇见的异常类型。
所谓内存泄漏,即我们对一个不会再被使用的对象保持着强引用。这时,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> 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 内存模型概述

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351