内存优化之内存泄漏

版权声明:如需转载请注明出处 https://blog.csdn.net/zhonglunshun/article/details/80550105

我们能通过这篇文章学到什么?我们这篇文章主要通过两点来讲:

  1. 探讨关于内存泄露的原因;
    1.1 什么是内存泄漏
    1.2 导致内存泄露的根本原因

  2. 实战找出内存泄漏并解决;
    2.1 怎么定位内存泄漏
    2.2 怎么解决内存泄漏

什么是内存泄漏

内存泄漏是指对象不再被应用程序使用,但是垃圾回收器却不能移除它们,因为它们正在被引用。为了方便大家理解,我用一个例子帮助大家理解垃圾回收机制;

大家去过食堂吃饭吧,吃完饭后,需要我们自己把盘子端过去回收;这就像C语言里面通过malloc开辟了一个内存区间,用完后需要手动回收一样;如果是下馆子,我们就不需要关心自己回收餐具的问题,吃完了我们擦擦嘴巴就能走人了,服务员会过来回收垃圾;这就像我们的Java虚拟机,能自动回收垃圾;但是这里存在一个问题,如果有人吃完饭赖着不走,土话说是占着茅坑不拉屎,这种情况怎么办?是不是需要看开发人员的素质;

内存也是一样的,当一个对象没有被引用的时候就会被jvm回收;那是不是可以反过来说,当对象被引用了就没办法被回收了?
答案是否定的。因为引用分为很多种,有强引用,软引用,虚引用,弱引用;还有一种情况,就是对象之间互相引用也是可以回收的;
怎么回答这个问题比较好呢?
回答:该对象一直往上追溯引用,能追溯到GC Root引用才能说不能被GC回收;我们来看一个图:
这里写图片描述
我们看到,左边的根节点是GC Root那么他不能被垃圾处理器回收,右边不是,所以可以被回收,关于垃圾回收机制大家可以参考一下网上的文章,其实这个东西说的有点大,要想真正熟悉的话看一本很厚的书都未必会真正了解,我们今天要讲的是内存泄露这块所以这里因为篇幅问题不多细讲了;

那么哪些对象可以作为GC Root呢?我们先来看一张图:
这里写图片描述
我们可以结合这张图一起看;
这里写图片描述
这个是jvm运行时的内存分配,这个jvm在加载一个类的时候,分配了几个内存区域,方法区,栈内存,本地方法区,pc寄存器,堆内存;

我们知道,具体的对象都是保存在堆里面,也是垃圾回收器清理的区域,如果这个里面的对象没有被其他地方引用或者只是被堆中的其他对象引用,那么这个对象就能够被回收;换句话说,除了堆内存中其他内存区有引用这个对象的话,那么这个对象就没办法被回收;

所以我们来总结一下:
可以作为GC Root的有:
1. java Stack中引用的对象;
2. 方法区中静态引用指向的对象;
3. 方法区中常量引用指向的对象;
4. Native方法区中Jni引用的对象;
5. Thread活着的线程;(Activity中开了一个新线程,Activity退出了,那么这个线程还活着没?活着的啊!)

实战找出内存泄漏并解决

  1. 检测项目中又没有内存泄漏的情况
    打开应用玩一下,退出,问现在这个app的进程还在不在?进程还在的啊,但是里面的activity,view是不是应该都被销毁了?,我们点击GC多次,然后我们看下现在的这个进程的内存占用情况;
    这里写图片描述

大家发现问题没有,这个里面有138个View,4个Context,2个Activity。是不是有大大的内存泄漏?大家想一下,一个View的内存消耗是很大的,这下大家知道为啥安卓手机虽然配置很高,但是经常要清理垃圾才能继续用,所以说到底不是我们的手机不行,而是谷歌提供给开发人员的api太灵活了,导致开发不规范;所以才有这些问题;
导出内存使用情况我们也可以通过命令行:
adb shell dumpsys meminfo [packagename] -d

刚刚给大家展示一些小小的甜点,接下来要真正展示干货了;
首先,运行安装我们的app,然后我们来用一下,写点东西,然后退出app;点击gc,把可以清理的垃圾都清理了;然后我们点击Monitor这一栏,我们发现Memory下面有几个按钮:
第一个是停止监控内存,第二个是强制gc,第三个是生成java 堆得快照,第四个是开始内存跟踪,我们点击第三个,在这之前可以多点几次gc防止有些垃圾还没被回收;

好了,点击快照之后,我们拿到了当前的内存快照,后缀名是.hprof的格式,我们来看下这里面都是些什么东西;
这个东西我们看下,类有点多啊,不是很好看,我们来切换一下,选为package tree view,我们对照每个字段解读一下:
内存快照
我们来看,这ui包里面有很多对象存在,点开进去,我们看到NoteEditActivity被很多地方引用了,存在3个他的实例,很明显它发生内存泄漏了;
可是,这只是我们的猜测,到底这个猜测对不对呢?我们通过什么方法,确定这个NoteEditActivity确实泄露了?

我们可以采用控制变量对比的方法,这个变量就是打开NoteEditActivity与否,如果打开这个Activity之后确实比没有打开之后出现了多的NoteEditActivity实例对象,就说明是在NoteEditActivity这个类里面导致了内存泄漏。大家觉得是不是这个道理;

那么怎么作对比呢?从eclipse开发转过来的朋友应该听说过mat,这是一个内存辅助分析工具,我这里也给大家下载过来了,大家来看下,这个东西怎么用呢?

我们首先把两个不同状态的内存快照保存并导出为标准文件的格式;导出来之后我们就能够进行对比了,我们把第一个hprof文件命名为hprof,第二个为hprof2;

我们切换到两个快照的分析表里面去,怎么做对比呢?在右边有一个对比的按钮,我们点击第一个表的对比按钮,会弹出一个框提示是否是和第二个表对比,我们点确认;
这里写图片描述

大家看,是不是第二个内存快照比第一个内存快照多出来了两个NoteActivity对象?那是不是说明发生了泄漏;那么接下来我们是不是只要找出NoteEditActivity里面泄漏的地方修正就行了;

怎么定位呢?

我们在第二张表里面找到NoteEditActivity,右击选择ListObject->withoutGoing Refrence表示列出这个activity被引用的地方(这个对象不能回收就是因为被引用了,所以选择outgoing);

这里写图片描述
对象下面的引用这么多,我们怎么定位到真正泄漏的地方呢?
我们前面有说到,被GC Root引用的对象才不能被GC回收,那么怎么判断这些引用哪些是Gc root,这里面有提供给我们一个工具,
这里写图片描述
排除后,只剩下一个引用,我们看下这个内容;
大家看这个NoteEditActivity的实例被CommonUtil引用了,我们看看CommonUtil干的好事;

public class CommUtil {
    private static CommUtil instance;
    private Context context;
    private CommUtil(Context context){
        this.context = context;
    }

    public static CommUtil getInstance(Context mContext){
        if(instance == null){
            instance = new CommUtil(mContext);
            //不适用Activity的上下文,而是使用Application的上下文。
            // 因为Application的生命周期长:进程退出的时候才会销毁,和Static的CommUtil的生命周期一样长。
        }
        return instance;
    }

大家看,commonUtil是一个静态对象,那他在这里就是一个GC Root节点,所以垃圾回收机制没办法回收这个Context对象,大家觉得有什么办法能够解决这个问题呢?
答案是在实例化CommonUtil的时候,使用全局的Application替代activity,具体是这样的:

 instance = new CommUtil(mContext.getApplicationContext());

原因是因为Application的生命周期长:进程退出的时候才会销毁,和Static的CommUtil的生命周期一样长。

我们继续看,NoteListActivity也发生了内存泄漏,同样的方法,我们来看下是哪里发生了泄漏;我们找到了他的GC Root引用点:
这里写图片描述
这个里面指向的不是我们写的代码,大家可以猜测一下,这里面有message,queue,ActivityThread;了解handler工作原理的朋友应该能猜到了,这很可能是由于handler使用不当导致内存泄漏;是不是这样呢?我们看下代码是不是handler导致的原因;

 private  Handler mHandler = new Handler();

    private void fetchData() {
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                onLoadSuccess();
            }
        },1000*60*60);
    }

我们看到activity里面有一个handler成员变量,而这个handler开启了一个子线程来请求数据;那么activity要销毁的时候,线程还在请求数据,handler开启了一个内部类来请求,那内部类是不是就持有了外部类的应用;

那么有没有办法解决这个问题呢?

有同学说把内部类改成静态的,可不可以呢?内部类如果变成静态的,是不是就没办法访问外部的方法了?那还有什么办法没有?

这种情况下,我们可以把这种强引用改为弱引用,怎么改?

 private  Handler mHandler = new Handler();

    private void fetchData() {
        mHandler.sendEmptyMessage(0);
    }

    private static class MyHandler extends Handler {
        private WeakReference<NotesListActivity> instance;

        public MyHandler(NotesListActivity activity) {
            instance = new WeakReference<NotesListActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            NotesListActivity aty = instance.get();
            if (aty == null || aty.isFinishing()) {
                return;
            }

            if(msg.what==0){
                aty.onLoadSuccess();
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }

这样的话,当activity被干掉的时候,虚拟机发现这是一个弱引用,于是就把这个activity回收了;这就相当于把GC Root的链接剪掉了,GC就能够回收了;

其实内存泄漏的解决方案还有很多很多,像资源未被关闭引起的内存泄漏,当使用了BroadcastReceiver,Cursor,Bitmap等资源时,不及时回收资源,就会引起内存泄漏,而这些东西都是很占内容的,久而久之就会越来越卡,大家看我们的安卓手机用着用着就卡了,也就是这个原因;

当然,这里只是给大家提供一个思路,大家发现啊,我们要去做内存泄漏,需要对工具使用非常熟悉,还需要了解源码和一些基础的原理,所以啊,大家出去面试的时候,面试官问的问题也是非常的刁钻,包括内存泄漏的啊;大家去面试会发现很多面试岗位,对性能的要求比较高;

今天跟大家分享这堂课的主要原因不仅仅是帮助大家事后找出泄露的地方并改正,更多的希望大家从源头上也就是在编码阶段明白哪些地方会导致内存泄漏从而养成良好的编码习惯,让我们写出更加稳定高效有质量的程序;这是对我们的产品负责,也是对我们自己负责;

这个我相信大家在开发这条路上走着走着就会发现,高质量的编码不仅仅是有利于面试拿到更高的薪资水平,更多的事自己的个人品牌,个人形象塑造好了,大家认可了,有了名气,有了人气之后,你出去都是别人挖着走的,面试这种事情,不需要的,也从不需要担心被低估,我们要是只是跳槽加工资,那么我们根本不需要学习很多深层次的东西;只需要把那几道面试题研究一下,因为面试的时候时间很短,面试官很难在短时间内了解一个人的真实水平,但是立足于长远来看,只有在一个行业把自己名气打起来了,才能在这个行业走的更远,飞得更高;

有一些学院问我,有一些半路出家培训了两三个月但是工资比他们高,表示很不服。我跟他们说,面试技巧只能用来应对入门阶段,开发这一行就是这样,很多做开发做了三年,发现和刚开始做的时候没有太大提升,以至于在面试的时候根本没有底气,然后深深预感到天花板的存在,大家知道开发不像业务员一样直接和客户打交道,那么当业务员客户积累了,后面可能就能挣大钱,开发也是这样,并不是搞个两三年就存在天花板了,工资没办法提升了,而是两三年后我们发现了我们要提升自己了,要从一个中初级的开发转向更高层次的开发管理,那么必然需要突破原有的打酱油的思维,否则会永远停留在低级水平,永远没得提升;

商业街有很多成功的商人是从开发转型过来的,很多互联网公司的老总,比如小马,雷布斯,丁磊等等,他们都不是停留在表面的开发,才能取得他们的成功,我们也是一样,不能只是停留在表面,这样只能骗自己,骗得了一时也骗不了一世,丑媳妇始终要见公婆,等到别人都做架构师,做管理了我们还是码代码的码农阶段,那是得不偿失的;

在这里我推荐一个专门帮助大家提高自身能力的网校,叫动脑学院,大家可以去网上搜一下,网上评价可能有褒有贬,具体还得看个人,如果是真心想学习点东西,我还是推荐的,毕竟分享的很多东西使我们确实不知道而且有必要知道的,当然大家如果有更好的也可以择其善者而从之;根本目的只有一个,就是让自己尽快得到提升,站在更高的视角看问题;然后你会发现,现在遇到的瓶颈都算不上瓶颈,往上还有更广阔的空间;

师傅领进门,修行靠个人,大家一起努力,只要目标明确就不怕风雨兼程,人成长最大的阻力来自于自身给的束缚,人都是有惰性的,要突破这个惰性需要很大的勇气,一旦突破,那就是更上一层楼了,希望大家越来越好,我也期待大家变得越来越好,今天就到这里,谢谢大家了;

猜你喜欢

转载自blog.csdn.net/zhonglunshun/article/details/80550105
今日推荐