内存泄漏

内存泄漏:指无用对象(不再使用的对象)持续占用内存使内存得不到及时地释放,从而造成的内存空间的浪费。
注意:内存泄漏持续积累将会导致内存溢出(OOM)的严重后果。

1. java中的内存泄漏基本知识

(1) java内存的分配策略

存储区 分配策略 存储的对象 内存分配时机 内存回收时机 备注
静态存储区(方法区) 静态分配 静态数据、全局变量等 程序编译前已分配内控空间 在程序运行期间都存在 -
栈区 栈分配 方法体内的局部变量 方法运行时在栈上创建内存空间 方法运行结束时自动释放(即越出变量作用域变量就被释放) 栈分配处理速度快(栈内存分配运算置于处理器中),但是栈内存容量小
堆区 堆分配(动态内存分配) new出来的对象以及数组 new创建对象时申请空间 不使用时,java的垃圾回收器自动回收 -

(2) java内存管理(对象的创建与释放)
对象创建(分配内存):开发人员通过new为每个对象在堆中申请空间
对象释放(释放内存):java的垃圾回收器(GC)自动回收

在java中,内存分配由开发人员来完成,内存释放由java的垃圾回收器自动完成, 这简化了我们的工作,但同时加重了java虚拟机的工作(java相较于c和c++运行速度更慢的原因之一)。
即使如此,GC也有着很大的作用:为了能够正确地释放对象,它需要检测每个对象的运行状态,包括对象的创建、引用、被引用和赋值等等。
GC工作原理:Java使用有向图的方式进行内存管理。我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某些对象 (连通子图)与这个根顶点不可达,那么我们认为这些对象不再被引用,可以被GC回收。

内存泄漏_第1张图片
内存管理有向图.png

2. android中的内存泄漏

2.1 单例模式引起的内存泄漏

原因分析:因为单例模式的静态特性导致单例对象的生命周期和应用的生命周期一样长,所以当无用对象被单例对象所持有,将导致无用对象在需要被释放时无法正常回收(内存泄漏)。

/**
 * Created by liy on 2019-11-15 9:01
 */
public class MemoryLeakTestActivity extends AppCompatActivity {

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

        UserManager userManager = UserManager.getInstance(this);

    }
    
}

  • 内存泄漏写法:若单例对象中持有activity的context,则当activity需要被释放时无法被正常回收
/**
 * Created by liy on 2019-11-14 16:29
 * 单例模式引起的内存泄漏:因为单例模式的静态特性导致单例对象的生命周期和应用的生命周期一样长,
 *                         所以当无用对象被单例对象所持有,将导致无用对象在需要被释放时无法正常回收(内存泄漏)
 */
public class UserManager {
    private Context context;
    private UserManager(Context context){
        this.context = context;//使用activity的context:造成activity需要释放时无法正常回收(内存泄漏)
    }

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

}
  • 解决内存泄漏:单例对象中使用application的context,而不要使用activity的context
 private UserManager(Context context){
        //this.context = context;//使用activity的context:造成activity需要释放时无法正常回收(内存泄漏)
        this.context = context.getApplicationContext();//使用application的context:单例对象的生命周期和应用的生命周期一样长,所以不会造成内存泄漏
}

2.2 非静态匿名内部类的静态实例引起的内存泄漏(如:Thread 和 监听事件回调)

(1) Thread

  • 内存泄漏写法:匿名内部类Runnable会持有对当前activity的引用,导致当activity需要被释放时无法被正常回收
/**
 * Created by liy on 2019-11-15 9:01
 */
public class MemoryLeakTestActivity extends AppCompatActivity {

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

        LoadData();
    }

    /**
     * 非静态匿名内部类的静态实例造成的内存泄漏
     * 内存泄漏原因分析:匿名内部类Runnable持有对当前Activity的隐式引用
     *                  当Activity销毁的时候(如返回键),若Runable内部的任务还未完成,那么将导致Activity的内存资源无法回收,造成内存泄漏。
     */
    private void LoadData(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(2000);
                Log.v("LoadData","非静态匿名内部类的静态实例造成的内存泄漏");
            }
        }).start();
    }
}

  • 解决内存泄漏:使用静态内部类(自定义静态内部类MyRunnable并实现Runnable)
 private void LoadData(){
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable).start();
    }

 private static class MyRunnable implements Runnable{//自定义静态内部类MyRunnable并实现Runnable,然后在使用的时候实例化
        @Override
        public void run() {
            SystemClock.sleep(2000);
            Log.v("LoadData","成功解决非静态匿名内部类的静态实例造成的内存泄漏问题");
        }
 }

2.3 非静态内部类的静态实例引起的内存泄漏(如:Handler 和 AsyncTask)

备注:在java中,非静态内部类非静态匿名内部类都会隐式地持有外部类的引用,而静态内部类不会持有外部类的引用。
原因分析:若在非静态内部类中创建静态实例,则该实例的生命周期和应用一样长, 导致该静态实例一直持有acticity,使activity无法被释放。

/**
 * Created by liy on 2019-11-15 9:01
 */
public class MemoryLeakTestActivity extends AppCompatActivity {

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

    /**
     * 内部类:当外部类activity启动时,都会使用内部类中的数据;虽然避免了资源的重复创建,但这种非静态内部类写法会造成内存泄漏
     * 内存泄漏原因分析:在java中非静态内部类默认持有外部类的引用,
     *                  若在非静态内部类中创建静态实例,则该实例的生命周期和应用一样长,
     *                  导致该静态实例一直持有acticity,使activity无法被释放
     * 解决内存泄漏:在java中静态内部类不会持有外部类的引用,故可将内部类设为静态内部类
     */
    /*class Resource{//非静态内部类默认持有外部类的引用,导致内存泄漏问题
        private static final String des1 = "";//在非静态内部类中创建静态实例,则该实例的生命周期和应用一样长,导致该静态实例一直持有acticity,使activity无法被释放
    }*/
    static class Resource{//静态内部类不会持有外部类的引用,避免了内存泄漏问题
        private static final String des2 = "";
    }

}

(1) Handler造成的内存泄漏-本质是非静态内部类中的静态实例引起的内存泄漏

  • 内存泄漏写法:非静态内部类(Handler)的实例(myHandler)持有外部类(activity)的引用,当activity需要被释放时,如果还有消息未处理,而消息关联着Handler,Handler又持有activity,将导致activity无法被回收。
/**
 * Created by liy on 2019-11-15 9:01
 */
public class MemoryLeakTestActivity extends AppCompatActivity {

    private TextView textView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.text);
    }

    @Override
    protected void onResume() {
        super.onResume();
        new MyThread().start();
    }

    private class MyThread extends Thread{
        @Override
        public void run() {
            super.run();
            try{
                sleep(5000);
            }catch (Exception e){
                e.printStackTrace();
            }
            Message message = new Message();
            message.what = 1;
            myHandler.sendMessage(message);
        }
    }

    /**
     * Handler造成的内存泄漏:myHandler其实是非静态内部类Handler的实例,所以它持有外部类acticity的引用
     *  分析:myHandler和activity的生命周期不一样,当activity退出需释放activity时,如果消息队列中还有消息未处理,那么将导致myHandler所持有的activity无法被回收
     */
    private Handler myHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case 1:
                    //Toast.makeText(MemoryLeakTestActivity.this,"更新UI",Toast.LENGTH_SHORT).show();
                    Toast.makeText(activity,"更新UI",Toast.LENGTH_SHORT).show();
            }
        }
    };

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

  • 解决内存泄漏:静态内部类 + 弱引用 + 回收资源
/**
 * Created by liy on 2019-11-15 9:01
 */
public class MemoryLeakTestActivity extends AppCompatActivity {

    private TextView textView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.text);
    }

    @Override
    protected void onResume() {
        super.onResume();
        new MyThread().start();
    }

    private class MyThread extends Thread{
        @Override
        public void run() {
            super.run();
            try{
                sleep(5000);
            }catch (Exception e){
                e.printStackTrace();
            }
            Message message = new Message();
            message.what = 1;
            myHandler.sendMessage(message);
        }
    }

    private MyHandler myHandler = new MyHandler(this);
    private static class MyHandler extends Handler{//(1)静态内部类:自定义静态内部类MyHandler并继承Handler,然后在使用的时候实例化
        private WeakReference reference;//(2)弱引用:MyHandler内部持有activity的弱引用
        public MyHandler(Context context){
            reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MemoryLeakTestActivity activity = (MemoryLeakTestActivity) reference.get();
            if (activity != null){
                activity.textView.setText("成功解决Handler造成的内存泄漏问题");
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        myHandler.removeCallbacksAndMessages(null);//(3)资源回收:在activity的onDestory()生命周期函数中调用handler.removeCallbacks()方法
    }
}

(2) AsyncTask造成的内存泄漏-本质是非静态内部类中的静态实例引起的内存泄漏

  • 解决内存泄漏(回收资源):在activity的onDestory()方法中调用asyncTask.onCancelled()回调方法来取消任务
    参考:异步消息处理机制
package comi.example.liy.mytestdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;

/**
 * Created by liy on 2019-11-07 17:10
 */
public class AsyncTaskTestActivity extends AppCompatActivity {

    private Button button;
    private ProgressBar progressBar;
    private TextView textView;
    
    private AsyncTaskUpdateInfo asyncTask;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_asynctask);

        button = (Button)findViewById(R.id.button03);
        progressBar = (ProgressBar)findViewById(R.id.progressBar02);
        textView = (TextView)findViewById(R.id.textView01);

        button.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                asyncTask = new AsyncTaskUpdateInfo(textView, progressBar);
                asyncTask.execute(1000);
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        asyncTask.onCancelled();//在activity的onDestroy()方法中取消asyncTask任务避免内存泄漏
    }
}

2.4 资源未关闭导致的内存泄漏

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

  • 服务(Service):关闭/解绑服务
    备注:Service是进程优先级比较低的服务进程,会影响内存回收,可用IntentService替代Service。Service运行在主线程不能直接进行耗时操作(需开线程),IntentService内部已创建了子线程,可通过它的onHandleIntent()来进行耗时操作;同时IntentService不同手动关闭,耗时任务结束后会自动关闭,避免了内存泄漏。
  • 广播(Braodcast):回收广播接收者
  • ContentObserver
  • 文档(File)
  • 游标(Cursor)
  • I/O流(steam)
  • 套接字(socket)
  • 位图(Bitmap):bitmap需调用bitmap.recycle()方法释放c端的内存

2.5 其他内存泄漏问题

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

分析:成员变量被声明为static则它的生命周期和app是一致的,若app被设计为常驻内存,则即使app被切到后台static成员变量也不会被释放。而app内存管理机制要求占内存较大的后台进程将优先被回收,所以当进程被回收时变量是不安全的。
解决:类设计时考虑变量是否真的需要在初始化时就设为静态成员,考虑是否可以用懒加载,避免使用static变量;如果设计上一定要用到static变量,需要对这些static变量的生命周期管理起来。

  • ListView优化

3. 拓展

(1)内存泄漏检测工具: Android/Java内存泄露检测框架 LeakCanary

你可能感兴趣的:(内存泄漏)