Android内存泄露

来源:互联网 发布:java导出word文档 编辑:程序博客网 时间:2024/04/29 22:42

本篇博客主要是记录一下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,基本上就能了解如何使用了。

1 0
原创粉丝点击