Android内存泄露

本篇博客主要是记录一下Android内存泄露相关的问题。
网上有很多介绍内存泄露的文章,自己摘取了一些比较有价值的内容,
同时增加了一些自己的理解。

在分析内存泄露前,我们必须先对内存泄露的定义有所了解。
简单来讲,Android对内存泄露的定义,与Java中的定义基本一致,即:
正常情况下,当一个对象已经不需要再被使用时,它占用的内存就能够被系统回收。
如果一个本该被回收的无用对象,由于被其它有效对象引用,使得对应的内存不能被系统回收,就称之为内存泄漏。

我们知道Android系统为每个应用分配的内存有限,当一个应用中产生的内存泄漏比较多时,
就难免会导致应用所需要的内存超过系统分配的极限,于是就造成应用出现了OOM错误。
因此,每个开发人员有必要对内存泄露的原理、出问题的场景及分析工具有一定的了解。

一、Java内存分配策略
Java 程序运行时的内存分配策略有三种,分别是静态分配、栈式分配和堆式分配。
对应的,三种分配策略使用的内存空间分别是静态存储区、栈区和堆区。
其中:
1、静态存储区:主要存放静态数据和常量。
这部分内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

2、栈区 :当方法被执行时,方法体内的局部变量都在栈上创建,并在方法执行结束时,自动释放其持有的内存。
由于栈内存分配相关的运算,内置于处理器的指令集中,因此执行效率很高,不过其分配的内存容量有限。

3、堆区 : 主要用于存储动态分配的对象。
这部分内存在不使用时,将会由 Java 的垃圾回收器来负责回收。

举例来说:

public class Example {
    //静态存储区
    static int e1 = 0;

    //堆区
    int e2 = 1;
    Example e3 = new Example();

    void method() {
        //栈区
        int e4 = 2;
        Example e5 = new Example();
    }
}

如上面代码所示,e1作为静态变量,是与Example这个类关联的,将被分配到静态存储区。

e2和e3均是一个具体对象的成员变量,由于对象必须被动态创建出来,因此e2和e3均将被分配到堆区。
即类中定义的非静态成员变量全部存储于堆中,包括基本数据类型、引用和引用指向的对象实体。

对于一个具体的方法来说,如代码中的method,当方法执行时,其内部的临时变量e4、e5均分配在栈区;
当方法执行完毕后,e4和e5的内存均会被自动释放。
这里需要注意的是,e5分配在栈区,但其指向的对象是分配在堆区的。
由此可以看出,局部变量的基本数据类型和引用存储于栈区,引用指向的对象实体存储于堆区。

二、Java内存释放策略
1、原理
Java的内存释放策略是由GC(Garbage Collection)机制决定和执行的,主要针对的是堆区内存。
GC为了能够准确及时地释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等。

关于GC释放内存的原理,参考了一些资料,个人觉得一种比较好的理解方式是:
将堆区对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。
将每个线程对象作为一个图的起始顶点,从起始顶点可达的对象都是有效对象,不会被GC回收;
如果某个对象从起始顶点出发不可达,那么这个对象就可以被认为是无效的,可以被 GC 回收。

对于程序运行的每一个时刻,都可以用一个有向图表示JVM的内存分配情况。
举例来说,对于下面的代码:

public class Solution {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Test();
        o2 = o1;
        //运行到此处,看一下对应的GC有向图
        .................
    }
}

public class Test {
    private Object o3;
    Test() {
        o3 = new Object();
    }
}

程序运行到注释行时,对应的GC有向图大致如下:

main函数所在进程为有向图的根节点。
当函数运行到注释行时,没有从根节点到Test对象和Obj 3的路径,
因此Test和Obj 3对象均可以被GC回收。
注意对象能否被回收的依据是,是否存在从根节点到该对象的路径,
因此虽然Obj 3被引用了,但依然会被回收。

由上述例子可以看出,Java的GC机制使用有向图的方式进行内存管理,可以消除引用循环的问题。
例如有三个对象相互引用,只要它们对根进程而言是不可达的,那么GC也可以对它们进行回收。
GC的这种内存管理方式的优点是精度很高,但是效率较低。
另外一种常用的内存管理技术是使用计数器,它与有向图相比精度较低(很难处理循环引用的问题),但执行效率较高。

最后需要提一点的是,对于程序员来说,GC对应的操作基本是透明的。
虽然可以主动调用几个GC相关的函数,例如System.gc()等,但是根据Java语言规范定义,
这些函数并不保证GC线程一定会进行实际的工作。
这是因为不同的JVM可能使用不同的算法管理GC,例如:
有的JVM检测到内存使用量到达门限时,才调度GC线程进行工作;
有的JVM是定时调度GC线程进行工作等。

2、Java内存泄露的例子
结合Java的GC机制,我们知道了,对于Java中分配在堆内存的对象而言:
如果某个对象是可达的,即在有向图中,存在从根节点到这些对象的路径;
同时这个对象是无用的,即程序以后不会再使用这个对象;
那么该对象就被判定为Java中的内存泄漏。

关于Java内存泄露的例子,可以参考下面的代码:

public class Example {
    private static ArrayList<Object> testArray = new ArrayList<>();

    public void test() {
        Object o = new Object();
        testArray.add(o);

        //本意是想主动释放掉内存,但Obj被testArray持有引用
        //因此,对应的堆内存无法释放掉
        o = null;
    }
}

类似上面的例子,如果集合类是全局性的变量,同时没有相应的删除机制,则很可能导致集合所占用的内存只增不减。

三、Android中的内存泄露举例
接下来我们看看Android中一些内存泄露的例子。

1、静态单例对象引入的泄露
静态对象生命周期的长度与整个应用一致,
如果静态对象持有了一个生命周期较短的对象,
例如Activity等,那么就会导致内存泄露。

这种错误经常出现在使用单例对象的场景中,例如:

public class SingleInstance {
    private final static Object LOCK = new Object();

    //单例模式需要静态对象
    private static SingleInstance singleInstance;

    //静态对象持有Context就可能导致内存泄露
    private Context mContext;

    public static SingleInstance getInstance(Context context) {
        synchronized (LOCK) {
            if (singleInstance == null) {
                return new SingleInstance(context);
            }
            return singleInstance;
        }
    }

    private SingleInstance(Context context) {
        mContext = context;
    }
}

如上面的代码所示:
如果获取单例模式时传的是Application的Context,
由于Application的生命周期就是整个应用的生命周期,
即Context与静态对象的生命周期一致,没有任何问题;

如果传入的是 Activity 等的 Context,那么当这个 Context 所对应的 Activity 退出时,
由于该 Context 的引用被静态单例对象所持有,而单例对象将持续到应用结束,
于是即使当前 Activity 退出,它的内存也不会被回收,就造成了内存泄漏。

由此可以看出,在Android中尽量不要让静态对象持有Context。
如果静态对象一定要持有Context,就让它持有Application Context,
即上面代码需要更改为:

public class SingleInstance {
    private final static Object LOCK = new Object();

    //Android Studio的静态代码检查,会提示不要将Context类置于静态引用中,可能会导致内存泄露
    private static SingleInstance singleInstance;

    private Context mContext;

    public static SingleInstance getInstance(Context context) {
        synchronized (LOCK) {
            if (singleInstance == null) {
                return new SingleInstance(context);
            }
            return singleInstance;
        }
    }

    private SingleInstance(Context context) {
        //若实在需要用,就获取Application Context
        mContext = context.getApplicationContext();
    }
}

不过Application Context也不是万能的,有些场景下Application Context是无法使用的,例如创建一个Dialog。
关于Application、Activity和Service三者的Context应用场景,自己也没有总结过,
就截一下参考资料中的图吧,有机会再深入研究一下:

图中,NO1表示 Application 和 Service 可以启动一个 Activity,不过需要创建一个新的 task 任务队列。

2、非静态内部类引入的泄露
如下代码所示,在MainActivity的onCreate函数中,创建了一个非静态内部类对象,
该对象被Activity中的一个静态对像引用。

public class MainActivity extends AppCompatActivity {
    private static Resource mResource = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (mResource == null) {
            mResource = new Resource();
        }
    }

    private class Resource {
        //........
    }
}

在上述代码对应的场景中,由于非静态内部类默认会持有外部类的引用,
而内部类的一个实例又被一个静态对象持有,于是最终导致外部类Activity被一个静态对象持有。、
正如前文提及的,由于静态对象一直存在,于是Activity退出时,对应的内存也没法被GC机制回收。

这种问题的解决方案就是,将非静态内部类变为静态内部类,或抽取成一个单独的类。

3、自定义Handler引入的泄露
非静态内部类引入内存泄漏的场景中,比较典型的就是自定义Handler引入的内存泄露:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //这种方式获取Handler,实际上绑定的是sThreadLocal中的Looper
        Handler handler = new MayLeakHandler();

        //延迟处理一个消息
        //这里匿名内部类其实也会持有外部类的引用
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                //.......
            }
        }, 6000);

        //Activity界面关闭,但其内存还是将被Handler对应的静态线程持有
        finish();
    }

    //定义一个非静态内部类
    private class MayLeakHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            //.............
        }
    }
}

上面代码产生内存泄露的原因是:
MayLeakHandler是一个非静态内部类,持有对Activity的引用。
当Activity退出时,MayLeakHandler仍被有效对象引用,
于是Activity对应的内存也无法被释放。

为了比较好的理解这个问题,我们看看Handler涉及到的一些源码。

当创建Handler时,最终调用的源码片段如下:

public Handler(Callback callback, boolean async) {
    ............
    //调用sThreadLocal.get(),即从应用主线程获取Looper
    mLooper = Looper.myLooper();
    ............
    //获取主线程的MessageQueue
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

当Handler发送消息或Runnable对象时,最终将调用到如下源码:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    //this指handle,即Msg将持有handler
    msg.target = this;
    ..........
    //Msg被加入到MessageQueue中,被MessageQueue持有
    return queue.enqueueMessage(msg, uptimeMillis);
}

根据上面的源码可以看出,当定义一个非静态内部类Handler时,
该Handler将被应用主线程的MessageQueue持有;
而Handler又持有了Activity的引用,于是即使Activity界面结束,
若Msg被有被处理掉,MessageQueue将一直持有Activity导致内存泄露。

对于上面那种使用Handler的方式,通常的修改方式是:

public class MainActivity extends AppCompatActivity {
    private MayLeakHandler mHandler = new MayLeakHandler(this);

    //Runnable也必须变成静态的,否则也会内存泄漏
    private static Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            //.......
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mHandler.postDelayed(mRunnable, 6000);

        finish();
    }

    private static class MayLeakHandler extends Handler {
        //如果MayLeakHandler需要访问Activity中的变量,就持有Activity的弱引用
        //这样垃圾回收时,就可以清除Activity的内存
        private WeakReference<Activity> mActivity;

        MayLeakHandler(Activity activity) {
            super();
            mActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            if (mActivity.get() != null) {
                //.............
            }
        }
    }

    @Override
    protected void onDestroy() {
        //最后根据需要需要,在Activity的onDestroy中清除mHandler处理的Message和Runnable
        //也可以调用其它接口单独清理msg或runnable
        //对于这个例子,Runnable是静态的,所以不需要
        //但其它情况msg和runnable可能会持有Activity,所以需要清理
        mHandler.removeCallbacksAndMessages(null);
        super.onDestroy();
    }
}

4、匿名内部类引入的泄露
匿名内部类也会持有外部类的引用,
因此与非静态内部类一样,也有可能导致内存泄露,
上面例子中初始定义的匿名Runnable,就会导致这个问题。

比较一般的场景是,如果匿名内部类被异步线程持有,
当异步线程与外部类的生命周期不一致时,就会导致内存泄露。

举例如下:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                //.........
            }
        };

        //假设workThread是一个长期运行的HandlerThread
        WorkThread workThread = WorkThread.getWorkThread();
        Handler handler = new Handler(workThread.getLooper());
        handler.post(runnable);
    }
}

上面的代码中,定义了一个匿名内部类runnable,该runable对象持有对Activity的引用。
将该runnable对象递交给WorkThread处理时,workThread就会持有该runable对象的引用,进而持有Activity对象。
如果workThread之前在进行某个耗时操作,那么可能Activity结束时,runable对象还未执行完毕,
于是Activity对应的内存没有及时释放,导致内存泄露。

这种类型的问题的解决方法,可能只有将runable写成静态类或单独抽取成一个独立的类。

5、线程相关的内存泄漏
在界面中使用线程对象,稍不注意也会造成内存泄露:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        leakOne();
    }

    private void leakOne() {
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    SystemClock.sleep(1000);
                }
            }
        }.start();
    }
}

很明显,这个问题与匿名内部类引起的内存泄露一样,由于Thread持有对Activity的引用,
同时Thread一直在运行,因此当Activity结束时,对应内存也不会被释放,导致内存泄露的放生。

现在,我们修改一下代码:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        leakTwo();
    }

    private void leakTwo() {
        new LeakThread().start();
    }

    private static class LeakThread extends Thread{
        @Override
        public void run() {
            while (true) {
                SystemClock.sleep(1000);
            }
        }
    }
}

可以看到,现在Thread变成了一个静态内部类,不再持有对Activity的引用,
因此Activity退出后,对应的内存可以被释放掉。

然而,这段代码还是有问题。
Activity每次创建时,均会创建一个新的永不结束的Thread。
JVM会持有每个运行Thread的引用,因此Activity创建出的Thread将不会被释放掉。
于是,不断的关闭打开Activity,将导致JVM持有的Thread越来越多。

因此上述代码需要修改为:

public class MainActivity extends AppCompatActivity {
    private LeakThread mLeakThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        leakThree();
    }

    private void leakThree() {
        mLeakThread = new LeakThread();
        mLeakThread.start();
    }

    private static class LeakThread extends Thread{
        private boolean mRunning = false;

        @Override
        public void run() {
            mRunning = true;
            while (mRunning) {
                SystemClock.sleep(1000);
            }
        }

        void close() {
            mRunning = false;
        }
    }

    @Override
    protected void onDestroy() {
        mLeakThread.close();
        super.onDestroy();
    }
}

修改比较简单,就是在Activity结束时,主动停止Thread。

6、资源未关闭造成的内存泄漏
解决最后这一类的内存泄露,主要就是要注意编程细节了。

使用BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源时,
应该在不再使用时,及时关闭或者注销。

四、总结
对Android中的内存泄露就先总结到这里了。
如何避免内存泄露,在上述例子中已经有对应的解决方案了,此处就不做赘述。
总之,代码看到多写的多,自然会养成良好的编程习惯,死记硬背一些规则,效率肯定比较低。

最后提一下检测内存泄露的工具,MAT有很多的资料,此处不做说明了。
有兴趣的话,推荐大家看看LeakCanary,这个开源工具可以极大地节省分析内存泄露的时间。
LeakCanary: 让内存泄露无所遁形
LeakCanary 中文使用说明
看看中文使用说明和对应demo,基本上就能了解如何使用了。

Python网络爬虫与推荐算法新闻推荐平台:网络爬虫:通过Python实现新浪新闻的爬取,可爬取新闻页面上的标题、文本、图片、视频链接(保留排版) 推荐算法:权重衰减+标签推荐+区域推荐+热点推荐.zip项目工程资源经过严格测试可直接运行成功且功能正常的情况才上传,可轻松复刻,拿到资料包后可轻松复现出一样的项目,本人系统开发经验充足(全领域),有任何使用问题欢迎随时与我联系,我会及时为您解惑,提供帮助。 【资源内容】:包含完整源码+工程文件+说明(如有)等。答辩评审平均分达到96分,放心下载使用!可轻松复现,设计报告也可借鉴此项目,该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的。 【提供帮助】:有任何使用问题欢迎随时与我联系,我会及时解答解惑,提供帮助 【附带帮助】:若还需要相关开发工具、学习资料等,我会提供帮助,提供资料,鼓励学习进步 【项目价值】:可用在相关项目设计中,皆可应用在项目、毕业设计、课程设计、期末/期中/大作业、工程实训、大创等学科竞赛比赛、初期项目立项、学习/练手等方面,可借鉴此优质项目实现复刻,设计报告也可借鉴此项目,也可基于此项目来扩展开发出更多功能 下载后请首先打开README文件(如有),项目工程可直接复现复刻,如果基础还行,也可在此程序基础上进行修改,以实现其它功能。供开源学习/技术交流/学习参考,勿用于商业用途。质量优质,放心下载使用。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值