始终存在的 "内存泄漏"


一、什么是内存泄漏?

内存泄漏是指:当程序不再使用到内存,但释放内存失败,从而产生了无用的内存消耗。

内存泄漏并不是指物理上的内存消失,这里的内存泄漏是指由程序分配的内存,由于程序逻辑错误而导致程序失去了对该内存的控制,使得内存浪费。

1.1 Java 内存分配策略

首先我们需要对 Java 的内存分配策略有一个基础的认知。

Java 程序运行时的内存分配策略有三种,分别是 静态分配栈式分配堆式分配

与其对应的三种存储策略使用的内存空间,分别是 静态存储区栈区堆区

我们看看三者的区别:

     ✎  静态存储区:主要存放 静态数据全局 static 数据常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

     ✎  栈区:当方法被执行时,方法体内的 局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。

     ✎  堆区:又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是 对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器(GC)来负责回收。

1.2 “栈” 与 “堆”

在方法体内定义的 基本类型的变量对象的引用变量 都是在方法的 栈内存 中分配的。例如:当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存 用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器 来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

举例说明:

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();

    public void method() {
        int s2 = 1;
        // Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,
        // 但 mSample2 指向的对象是存在于堆上
        Sample mSample2 = new Sample();
    }
}
// mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,
// 而它自己存在于栈中
Sample mSample3 = new Sample();        

1.3 Java 如何管理内存

Java 的内存管理其实就是对象的 “分配” 和 “释放” 问题。

在 Java 中,程序员需要通过关键字 new 为每个对象申请内存空间(基本类型除外),所有的对象都在堆(Heap)中分配空间。另外,对象的释放是由 GC 决定和执行的。在 Java 中,内存的分配是由程序完成的,而内存的释放是由 GC 完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了 JVM 的工作。这也是 Java 程序运行速度较慢的原因之一。因为,GC 为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

二、常见内存泄漏

接下来,我们探讨在实际的开发过程中,可能存在内存泄漏的场景。

2.1 永恒的单例

“单例” 是常用的一种设计模式,使用单例模式的类,只会产生一个对象,这个对象看起来像是一直占用着内存,但这并不意味着就是浪费了内存,内存本来就是拿来装东西的,只要这个对象一直都被高效的利用就不能叫做泄露。

但是由于 单例的“静态特性”使得其生命周期跟应用的生命周期一样长,不正确的使用会导致无限制的持有 Activity 的引用,从而将造成内存泄漏。

比如,我们看下面这个例子:

public class SingleInstanceTest {

    private static SingleInstanceTest instance;
    private Context mContext;

    private SingleInstanceTest(Context context) {
        this.mContext = context;
    }

    public static SingleInstanceTest getInstance(Context context) {
        if (instance == null) {
            instance = new SingleInstanceTest(context);
        }
        return instance;
    }
}

这是一个普通的单例模式写法,当创建这个单例的时候,需要外部传入一个 Context 来获取该类的实例,这个 Context 生命周期的长短至关重要!(这一点在实际开发过程中最为常见)

1、如果此时传入的是 ApplicationContext,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。

2、如果此时传入的是 ActivityContext,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有(强引用),其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。针对一些比较大的 Activity,甚至还会导致 OOM(内存溢出)

✎ 正确的方式(写法一):常用

public class SingleInstanceTest {

    private static SingleInstanceTest instance;
    private Context mContext;

    private SingleInstanceTest(Context context) {
        this.mContext = context.getApplicationContext();    // 使用 Application 的 context
    }

    public static SingleInstanceTest getInstance(Context context) {
        if (instance == null) {
            instance = new SingleInstanceTest(context);
        }
        return instance;
    }
}

可以看到在 SingleInstanceTest 的构造函数中,将 context.getApplicationContext() 赋值给 mContext,此时单例引用的对象是 Application,而 Application 的生命周期本来就跟应用程序是一样的,也就不存在内存泄露。

✎ 正确的方式(写法二):

// 在你的 Application 中添加一个静态方法,getContext() 返回 Application 的 context

    context = getApplicationContext();
    ... ...

    /**
     * 获取全局的 context
     * @return 返回全局 context 对象
     */
    public static Context getContext(){
        return context;
    }

    public class SingleInstanceTest {

        private static SingleInstanceTest instance;
        private Context mContext;

        private SingleInstanceTest() {
            this.mContext = MyApplication.getContext();    // 使用 Application 的 context
        }

        public static SingleInstanceTest getInstance() {
            if (instance == null) {
                instance = new SingleInstanceTest();
            }
            return instance;
        }
    }

✎ 另外一种情况:

很多时候我们在需要用到 Activity 或者 Context 的地方,会直接将 Activity 的实例作为参数传给对应的类,就像这样:

public class Sample {

    private Context mContext;

    public Sample(Context context){
        this.mContext = context;
    }

    public Context getContext() {
        return mContext;
    }
}

// 外部调用
Sample sample = new Sample(MainActivity.this);

这种情况如果不注意的话,很容易就会造成内存泄露,比较好的写法是使用 弱引用(WeakReference)来进行改进。

public class Sample {

    private WeakReference<Context> mWeakReference;

    public Sample(Context context){
        this.mWeakReference = new WeakReference<>(context);
    }

    public Context getContext() {
        if(mWeakReference.get() != null){
            return mWeakReference.get();
        }
        return null;
    }
}

// 外部调用
Sample sample = new Sample(MainActivity.this);

被弱引用关联的对象只能存活到下一次垃圾回收之前,也就是说即使 Sample 持有 Activity 的引用,但由于 GC 会帮我们回收相关的引用,被销毁的 Activity 也会被回收内存,这样我们就不用担心会发生内存泄露了。

2.2 静态 Activity

我们看下面这段代码:

public class MainActivity extends AppCompatActivity {
    private static MainActivity activity;    // 这边设置了静态 Activity,发生了内存泄漏
    Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (TextView) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticActivity();
                nextActivity();
            }
        });
    }

    void setStaticActivity() {
        activity = this;
    }

    void nextActivity(){
        startActivity(new Intent(this, RegisterActivity.class));
        SystemClock.sleep(1000);
        finish();
    }

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

在上面代码中,我们声明了一个静态的 Activity 变量并且在 TextView 的 OnClick 事件里引用了当前正在运行的 Activity 实例,所以如果在 Activity 的生命周期结束之前没有清除这个引用,则会引起内存泄漏。因为声明的 Activity 是静态的,会常驻内存,如果该对象不清除,则垃圾回收器无法回收变量。

我们可以这样解决:

    protected void onDestroy() {
        super.onDestroy();
        activity = null;    // 在 onDestory 方法中将静态变量 activity 置空,这样垃圾回收器就可以将静态变量回收
    }

2.3 静态 View

静态 View 和静态 Activity 颇为相似,我们看下代码:

public class MainActivity extends AppCompatActivity {

    private static View view;    // 定义静态 View
    Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (TextView) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticView();
                nextActivity();
            }
        });
    }

    void setStaticView() {
        view = findViewById(R.id.view);
    }

}

View 一旦被加载到界面中将会持有一个 Context 对象的引用,在这个例子中,这个 context 对象是我们的 Activity,声明一个静态变量引用这个 View,也就引用了 Activity,所以当 Activity 生命周期结束了,静态 View 没有清除掉,还持有 Activity 的引用,因此就会发生内存泄漏。

我们可以这样解决:

protected void onDestroy() {
    super.onDestroy();
    view = null;    // 在 onDestroy 方法里将静态变量置空
} 

2.4 非静态内部类

我们先讨论下非静态内部类(non static inner class)静态内部类(static inner class)之间的区别。


class 对比 static inner class non static inner class
与外部 class 引用关系 如果没有传入参数,就没有引用关系 自动获得强引用
被调用时需要外部实例 不需要 需要
能否调用外部 class 中的变量和方法 不能
生命周期 自主的生命周期 依赖于外部类,甚至比外部类更长


可以看到非静态内部类自动获得外部类的强引用,而且它的生命周期甚至比外部类更长,这便埋下了内存泄露的隐患。如果一个 Activity 的非静态内部类的生命周期比 Activity 更长,那么 Activity 的内存便无法被回收,也就是发生了内存泄露,而且还有可能发生难以预防的空指针问题。

比如我们看以下代码:

public class MainActivity extends AppCompatActivity {

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

    class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}

可以看到我们在 Activity 中继承 AsyncTask 自定义了一个非静态内部类,在 doInbackground() 方法中做了耗时的操作,然后在 onCreate() 中启动 MyAsyncTask。如果在耗时操作结束之前,Activity 被销毁了,这时候因为 MyAsyncTask 持有 Activity 的强引用,便会导致 Activity 的内存无法被回收,这时候便会产生内存泄露。

正确的做法为:将 MyAsyncTask 变成静态内部类

public class MainActivity extends AppCompatActivity {

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

    static class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(50000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}

这时候 MyAsyncTask 不再持有 Activity 的强引用,即使 AsyncTask 的耗时操作还在继续,Activity 的内存也能顺利地被回收。

2.5 匿名类 / AsyncTask

匿名类和非静态内部类最大的共同点就是都持有外部类的引用,因此,匿名类造成内存泄露的原因也跟静态内部类基本是一样的,下面列 举几个比较常见的例子:

public class MainActivity extends AppCompatActivity {

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // ① 匿名线程持有 Activity 的引用,进行耗时操作
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(50000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // ② 使用匿名 Handler 发送耗时消息
        Message message = Message.obtain();
        mHandler.sendMessageDelayed(message, 60000);
    }

上面举出了两个比较常见的例子:

✎  new 出一个匿名的 Thread,进行耗时的操作,如果 MainActivity 被销毁而 Thread 中的耗时操作没有结束的话,便会产生内存泄露。

✎  new 出一个匿名的 Handler,这里我采用了 sendMessageDelayed() 方法来发送消息,这时如果 MainActivity 被销毁,而 Handler 里面的消息还没发送完毕的话,Activity 的内存也不会被回收。

我们可以这样解决:

✎  继承 Thread 实现静态内部类。

✎  继承 Handler 实现静态内部类,以及在 Activity 的 onDestroy() 方法中,mHandler.removeCallbacksAndMessages(null)。

2.6 Handler

Handler 的使用造成的内存泄漏问题应该说是 最为常见 了,很多时候我们为了避免 ANR 而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等 API 都借助 Handler 来处理,但 Handler 不是万能的,对于 Handler 的使用代码编写不规范即有可能造成内存泄漏。另外,我们知道 Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。

由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。

我们看下面的例子:

public class SampleActivity extends Activity {

    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    }

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

        // Post a message and delay its execution for 10 minutes.
        mLeakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() { /* ... */ }
        }, 1000 * 60 * 10);

        // Go back to the previous Activity.
        finish();
    }
}

在该 SampleActivity 中声明了一个延迟 10分钟 执行的消息 Message,mLeakyHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。

正确的做法为:

在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见下面代码:

public class SampleActivity extends Activity {

    private static class MyHandler extends Handler {
        private final WeakReference<SampleActivity> mActivity;

        public MyHandler(SampleActivity activity) {
            mActivity = new WeakReference<SampleActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            SampleActivity activity = mActivity.get();
            if (activity != null) {                              // 每次使用前注意判空
                // ...
            }
        }
    }

    private final MyHandler mHandler = new MyHandler(this);

    private static final Runnable sRunnable = new Runnable() {
        @Override
        public void run() { /* ... */ }
    };

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

        // Post a message and delay its execution for 10 minutes.
        mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

        // Go back to the previous Activity.
        finish();
    }
}

从上面的代码中我们可以看出如何避免 Handler 内存泄漏,推荐使用 静态内部类 + WeakReference 这种方式,每次使用前注意判空。

Java 对引用的分类有 Strong referenceSoftReferenceWeakReferencePhatomReference四种。


级别 回收机制 用途 生存时间
从来不会 对象的一般状态 JVM 停止运行时终止
在内存不足时 联合 ReferenceQueue 构造有效期短/占内存打/生命周期长的对象的二级高速缓冲器(内存不足时才情况) 内存不足时终止
在垃圾回收时 联合 ReferenceQueue 构造有效期短/占内存打/生命周期长的对象的一级高速缓冲器(系统发生gc时清空) gc 运行后终止
在垃圾回收时 联合 ReferenceQueue 来跟踪对象被垃圾回收期回收的活动 gc 运行后终止


在 Android 应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。

软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。

2.7 Timer Tasks

看个范例:

public class SampleActivity extends Activity {
    void scheduleTimer() {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                while(true);
            }
        },1000);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View ttButton = findViewById(R.id.tt_button);
        ttButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                scheduleTimer();
            }
        });
    }
}

这里内存泄漏在于 Timer 和 TimerTask 没有进行 Cancel,从而导致 Timer 和 TimerTask 一直引用外部类 Activity。

正确的做法为:

在适当的时机进行 Cancel。

2.8 Sensor Manager

看个范例:

public class SampleActivity extends Activity {
    void registerListener() {
           SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
           Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
           sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View smButton = findViewById(R.id.sm_button);
        smButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                registerListener();
            }
        });
    }
}

通过 Context 调用 getSystemService 获取系统服务,这些服务运行在他们自己的进程执行一系列后台工作或者提供和硬件交互的接口,如果 Context 对象需要在一个 Service 内部事件发生时随时收到通知,则需要把自己作为一个监听器注册进去,这样服务就会持有一个 Activity,如果开发者忘记了在 Activity 被销毁前注销这个监听器,这样就导致内存泄漏。

正确的做法为:

在 onDestroy 方法里注销监听器。

2.9 尽量避免使用 static 成员变量

如果成员变量被声明为 static,那我们都知道其生命周期将与整个 app 进程生命周期一样。

这会导致一系列问题,如果你的 app 进程设计上是长驻内存的,那即使 app 切到后台,这部分内存也不会被释放。按照现在手机 app 内存管理机制,占内存较大的后台进程将优先回收,如果此 app 做过进程互保保活,那会造成 app 在后台频繁重启。当手机安装了你参与开发的 app 以后一夜时间手机被消耗空了电量、流量,你的 app 不得不被用户卸载或者静默。

这里修复的方法是:

不要在类初始时初始化静态成员。可以考虑 lazy 初始化(使用时初始化)。架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。

2.10 集合类

我们通常会把一些对象的引用加入到集合容器(比如 ArrayList)中,当我们不再需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是 static 的话,那情况就更严重了。

比如下面这段程序:

   static List<Object> objectList = new ArrayList<>();
   for (int i = 0; i < 10; i++) {
       Object obj = new Object();
       objectList.add(obj);
       obj = null;
    }

在这个例子中,循环多次将 new 出来的对象放入一个静态的集合中,因为静态变量的生命周期和应用程序一致,而且他们所引用的对象 Object 也不能释放,这样便造成了内存泄露。

所以在退出程序之前,将集合里面的东西 clear,然后置为 null,再退出程序,如下:

 @Override
 public void onDestroy() {
     super.onDestroy();
     if (objectList != null){
         objectList.clear();
         objectList = null;
     }
 }

2.11 webView

当我们不再需要使用 webView 的时候,应该调用它的 destory() 方法来销毁它,并释放其占用的内存,否则其占用的内存长期也不能回收,从而造成内存泄漏。

正确的做法为:

为 webView 开启另外一个进程,通过 AIDL 与主线程进行通信,webView 所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

2.12 资源未关闭

对于使用了 BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap 等资源的使用,应该在 Activity 销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。


三、总结

在开发中,内存泄漏最坏的情况是 App 耗尽内存导致崩溃,但是往往真实情况不是这样的,相反它只会耗尽大量内存但不至于闪退,可分配的内存少了,GC 便会更多的工作释放内存,GC 是非常耗时的操作,因此会使得页面卡顿。我们在开发中一定要注意当在 Activity 里实例化一个对象时看看是否有潜在的内存泄漏,一定要经常对内存泄漏进行检测。