每日一道面试题(第3期)---一般什么情况下会导致内存泄漏问题

1,914 阅读5分钟

零零碎碎的东西总是记不长久,仅仅学习别人的文章也只是他人咀嚼后留下的残渣。无意中发现了这个每日一道面试题,想了想如果只是简单地去思考,那么不仅会收效甚微,甚至难一点的题目自己可能都懒得去想,坚持不下来。所以不如把每一次的思考、理解以及别人的见解记录下来。不仅加深自己的理解,更要激励自己坚持下去。

内存泄漏

定义

当本应该被释放或无用的对象,因为被其他存活的对象持有其引用,导致该对象不能被垃圾回收器回收,一直占用着内存,使程序运行变得缓慢甚至崩溃。

原因

为什么被其他存活的对象持有其引用,就不能被回收?这个就需要了解java的垃圾回收机制。

java垃圾回收机制

什么样的对象会被认为需要回收呢?我们现在将每一个对象看作有向图的结点,而对象之间的引用关系则是有向图的边。那么一定会有一个起始结点对象,如果这个对象是

  • 方法区的类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈中的native方法引用的对象
  • 虚拟机栈(栈帧中的本地变量表(局部变量表))所引用的对象

那么由此对象可以在有向图上遍历到的所有对象都不会被回收。反之,就会被认为是要回收的对象。

抽象的来说,一个程序中会存在许多这样的有向图,如果一个对象同时被两个存在起始结点对象的有向图所引用。当一个有向图完成使命,需要被销毁,但另一个有向图的生命周期还没有结束。那么这个本应该无用的对象,却不能被垃圾回收器回收,只有当另一个有向图生命周期结束,才会被回收。

所以,就是我们常说的生命周期不同的两个对象间有引用关系,生命周期短的可能会造成内存泄漏,持续的时间取决于生命周期长的对象。如果这个对象是静态变量,那么将会持续到整个程序运行结束。

Android内存泄漏情况

集合类

一般的集合类并不会造成内存泄漏,但是如果是全局性的集合类,如果不注意在使用完毕后进行remove操作,就极有可能造成内存泄露。

单例模式

这里的单例模式是指创建时需要传入Context作为参数。比如我们常写的下面这个代码。

public class Manager {
    private static Manager instance;
    private Context context;
    private Manager(Context context){
        this.context = context;
    }

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

关键就在于这个Context,如果这个Context是Activity的Content,那么显然Activity的生命周期和单例模式的对象的生命周期是不一样的,传入Content的Activity使用完毕需要被回收时,是无法被垃圾回收器回收的。

显而易见的,当这个Context是Application的时,就不存在内存泄漏的问题。因为单例模式的对象与Application的生命周期都是整个应用的生命周期,不会有任何问题。

所以,我们可以改为这样写

public class Manager {
    private static Manager instance;
    private Context context;
    private Manager(Context context){
        this.context = context.getApplicationContext();
    }

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

当然了,Application的Context也不是能随便用的。如果是要启动一个Activity,Application需要创建一个新的Task任务栈。而如果是创建一个Dialog,则只有Activity的context才可以。

匿名内部类

对于匿名内部类,在Android中典型的例子就是Handler了吧。这个我在第一期---自定义Handler如何有效保证内存泄漏问题已经说得很明白了。主要就是匿名内部类持有外部类的引用,匿名内部类的一些操作使得该内部类对象的生命周期和外部类的生命周期不相同,造成内存泄漏。

非静态内部类

在开发中,我们为了程序的高效以及资源重复利用,我们可能会经常写出这样的代码。

public class MainActivity extends BaseActivity {
    private static Resource resource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(resource == null){
            resource = new Resource();
        }
    }

    class Resource{
    }
}

这样做虽然有效的避免了资源的重复创建,每次在Activity启动时快速的使用这些资源,但却会造成内存泄漏。因为非静态内部类也默认会持有外部类的引用。而由于这个非静态内部类的静态实例,其生命周期会和整个应用程序一样长,所以会造成内存泄露。

解决办法就是将该内部类设为静态内部类,或者把这个内部类抽取出来封装成一个单例模式。

资源未关闭

在我们使用BroadcastReceiver、File、Course、Stream、ContentObserver等资源或者一些框架eventbus等明确表示需要Register与unRegister时,都应该在Activity被销毁时关闭或者注销,否则这些资源将不会被回收。

不良代码造成的压力

有时也并不是不能及时回收的对象造成的内存泄漏,而是有些代码没有及时有效的释放不需要使用的内存,或者是没有对于现有资源没有有效利用而频繁的申请新的内存,造成内存的巨大压力。

比如ListView中的ContentView,不使用ViewHolder有效的复用View而频繁的创建新的View,造成内存压力。