内存优化——内存泄漏

什么是内存泄漏?

程序中已动态分配的的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费。本质上是长生命周期的对象持有短生命周期对象的强引用,从而导致短生命周期对象无法被回收,则出现了内存泄漏的现象。
内存泄漏会带来以下几个问题:

  • 应用可用内存减少;
  • 频繁的gc,应用性能下降;
  • 严重时会发生内存溢出,即OOM Error;

为了了解内存泄漏,我们先来了解一下 Java 的引用类型。

Java中的几种引用类型

从jdk1.2版本开始把对象引用分成四种级别,从而使程序能更加灵活的控制对象生命周期,四种级别由高到底分别是强引用软引用弱引用虚引用,这里只介绍四种引用和gc的关系。

  • 强引用:无法被gc回收,当内存不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,也不会回收强引用对象。
  • 软引用:只有当内存不足时才会被回收,如果内存足够,gc扫描到软引用也不会回收它。
  • 弱引用:当gc线程扫描内存区域的时候,只要发现有弱引用就会回收它的内存,不管内存够不够用。
  • 虚引用:随时会被回收,不能通过虚引用来取得一个对象实例,一般用来跟踪对象被gc的时机。

了解了几种引用之后,可以发现除了强引用会导致内存泄漏之外,其实软引用同样也会,因为在内存够用的时候,软引用也是无法被gc回收的。而弱引用和虚引用则遇到gc就被回收,所以弱引用也是常用来避免内存泄漏的方法之一。

发生内存泄漏的场景

1、非静态内部类/匿名内部类
举个栗子:

public class MainActivity extends AppCompatActivity {

    // 匿名内部类 持有外部类MainActivity的引用
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
        }
    };

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

        // 1、匿名内部类 mHandler持有外部类 MainActivity的引用
        Message message = Message.obtain();
        mHandler.sendMessageDelayed(message, 50000);

        // 2、这里的 Handler不是匿名内部类, 但 Runnable是一个匿名内部类
        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 50000);

        // 3、这里的 MyHandler是一个内部类,持有外部类引用
        MyHandler myHandler = new MyHandler();
        myHandler.sendMessageDelayed(message, 50000);
    }

    class MyHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }

}

Handler是很容易发生内存泄漏的工具,这里举了3个内存泄漏的例子,原因就是非静态内部类持有外部类对象的强引用,这时MainActivity退出后想要回收内存,但是Handler的任务还在等待依然持有MainActivity的强引用,内存无法回收,于是发生了内存泄漏。

解决方式:

  • 在Activity的onDesdory方法中移除任务。
    mHandler.removeCallbacksAndMessages(null);
  • 使用静态内部类,静态内部类不持有外部类引用,如果真的需要使用外部类对象,要用弱引用WeakReference包装,注意弱引用在使用时记得判空,因为它被gc扫到会直接回收内存。
   static class MyHandler extends Handler{
       WeakReference mActivity;

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

       @Override
       public void handleMessage(Message msg) {
           super.handleMessage(msg);
           MainActivity mainActivity = mActivity.get();
           // 弱引用在使用时记得判空
           if (mainActivity != null){
               System.out.println(mainActivity.toString());
           }
       }
   }

Tip:这里要注意,并不是所有的非静态内部类和匿名内部类都会发生内存泄漏,只是他们持有了外部类的引用,如果他们的对象被其他生命周期更长的对象持有,外部类的对象就间接被持有不能及时得到回收,才会导致内存泄漏。Handler中发送延迟消息时,Handler对象和Runnable对象都会被消息队列持有,他们又持有Activity对象,所以Activity退出时无法及时回收。

2、单例或者静态成员

public class MyManager {

    private volatile static MyManager sManager;
    private Context mContext;

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

    public static MyManager getInstance(Context context){
        if (sManager == null){
            synchronized (MyManager.class){
                if (sManager == null){
                    sManager = new MyManager(context);
                }
            }
        }
        return sManager;
    }
}

这是一个标准的单例类,构造方法要传入一个Context类型的参数,如果我们传入的是Activity对象,很可能在这个Activity已经关闭了,MyManager还持有它的强引用,因为静态变量的生命周期是和应用生命周期一致的,自然这里就发生了内存泄漏。

解决方案

  • 获取Application对象。
    private MyManager(Context context) {
        mContext = context.getApplicationContext();
    }
  • 弱引用保护。
    private WeakReference mContext;

    private MyManager(Context context) {
        mContext = new WeakReference<>(context);
    }

3、集合类

    List list = new ArrayList<>();
    
    private void addObj(){
        Object obj = new Object();
        list.add(obj);
    }

如果集合添加了一些对象,后来对象需要销毁回收,此时集合中依然持有他们的强引用,比如观察者模式或者EventBus,当集合中的对象要销毁时需要及时remove掉,避免内存泄漏。

4、其他情况
除了以上几种常见情况,还有:

  • 资源未关闭,如FileOutputStream未close,数据库游标Cursor未close等;
  • 注册的资源未反注册,如广播、EventBus,RxBus等等
  • 静态成员使用完及时释放置空。
  • webview的内存泄漏,可以把webview单独放在一个进程中,不占用主进程内存。
  • ......

内存泄漏的检测

1、Profiler + MAT
模拟一个简单的内存泄漏例子:

public class SecondActivity extends AppCompatActivity {

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

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                
            }
        },10000);
    }
}

就是上面举过的匿名内部类的内存泄漏例子,把它放在SecondActivity中,多次打开关闭这个activity,然后看Profiler:

检测内存泄漏.png

多次操作后,dump一段此时的内存信息,通过包名查找可以看到内存中有5个SecondActivity对象,明显是不正常的,因为我们现在已经退出了这个页面,但是没看代码的情况下,一定能判定发生了内存泄露吗?不一定,因为内存泄漏只有强引用和软引用时会发生,这里也可能是弱引用,只是gc还没有扫到这块内存区域,没有回收它。这时光用Profiler已经无法继续辨别它的引用类型,MAT(Memory Analyzer tool)就排上用场了。
上图的左上方有个保存的按钮可以将这段内存信息保存到一个文件中:
保存的内存信息文件.png

但是这个文件是不能直接被MAT工具打开的

mat报错.png

需要用到另一个工具转化文件格式:hprof-conv;这个工具已经集成在Android SDK中了

hprof-conv.png

要用这个工具需要先给他配置环境变量


配置环境变量.png

配置完成测试一下

image.png

路径切换到之前保存的内存文件路径下,输入命令转换格式:
hprof-conv memory-20191023T160433.hprof 1.hprof

格式转换.png

生成新文件 1.hprof

image.png

最后再使用MAT打开1.hprof文件

mat.png

终于可以看到对象信息了,点开Histogram查看内存中所有对象信息

所有对象信息.png

搜索SecondActivity,筛选掉软弱虚引用

image.png
image.png

筛选后还剩下2个强引用的对象,打开详情我们从最后一行开始看引用关系,
SecondActivity --> this$0 -->callback --> next -->mMessages -->mQueue -->sUiHandler
其中 this$0 就是SecondActivity 中的匿名内部类 Runnable对象,在创建匿名内部类对象时,外部类的对象被当作参数传递进去,这里就持有了外部类的强引用;最后被sUiHandler引用,从命名上就能看出它是一个静态变量,生命周期和应用一致,我们关闭SecondActivity 到这里就发生了内存泄漏。

2.LeakCanary

使用MAT来分析内存问题,有一些门槛,会有一些难度,并且效率也不是很高,对于一个内存泄漏问题,可能要进行多次排查和对比才能找到问题原因。 为了能够简单迅速的发现内存泄漏,Square公司基于MAT开源了LeakCanary。
LeakCanary接入项目后,在app测试运行期间就可以检测到内存泄漏,并生成日志,查看起来非常方便,后面会单独写一篇LeakCanary使用及源码分析的文章。

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