内存泄露浅析(一)

什么是内存泄露

当一个对象不再被程序使用,但是垃圾回收器却无法回收它们,因为它们正在被引用。这种现象就被称为内存泄漏(Memory Leak)。
要理解内存泄露的概念,我们需要理解对象未被使用和对象未被引用这两个概率的区别。我们先来看一个小例子:

public static class SimpleStack {
    private static final int STACK_SIZE = 10;
    private final Obj[] objectPool = new Obj[STACK_SIZE];
    private int pointer = -1;
  
    public Obj pop() {
        if(pointer < 0) {
            throw new IllegalStateException("no elements on stack");
        }
        return objectPool[pointer--];
    }
  
    public Obj peek() {
        if(pointer < 0) {
            throw new IllegalStateException("no elements on stack");
        }
        return objectPool[pointer];
  
    }
  
    public void push(Obj object) {
        if(pointer >= (STACK_SIZE-1)) {
            throw new IllegalStateException("stack overflow");
        }
        objectPool[++pointer] = object;
    }
}
public static class Obj {
    private String name = null;
    public Obj(String name){
        this.name = name;
    }
    public String getName(){
        return name;
    }
}
public class Main {
    public static void main(String[] args){
        SimpleStack stack = new SimpleStack();
        stack.push(new Obj("A"));
        stack.push(new Obj("B"));
        stack.push(new Obj("C"));
        stack.push(new Obj("D"));
        
        Obj o = stack.pop();
        System.out.print("o's name is " + o.getName());
        System.out.print("o name's length is " + o.getName().length());
        o = new Obj("E");
    }
}

上面的例子中SimpleStack类实现了一个基于数组的栈,并维护了一个用于指向栈内当前可用单元的整型指针。 main函数先创建一个栈对象,然后往栈中push了A、B、C、D四个对象(暂以对象的name属性来称呼之),随后取出对象D赋值给o,之后输出D的name属性以及name属性的长度。之后便没再用到D对象了(因为o = new Obj("E"))。这时候对象D就处于 未使用 状态。
这是不是意味着D可以被垃圾回收器回收了呢?不行,因为D对象还被其他对象引用着,这个对象就是SimpleStack类的成员变量objectPool 。 因为SimpleStackpop方法仅仅是把栈顶元素返回、栈内指针下移,却并没有将对象移出栈,所以objectPool数组会继续持有D对象的引用,所以JVM不能回收这个对象。这就是内存泄露。

我们再来看一个Android方面的例子:

public class ExampleActivity extends Activity {
    @Override
    public void onCreate(Bundle bundle) {
        startActivity(new Intent(this, Example2Activity.class).putExtra("key",
                new Serializable() {
                    public String getInfo() {
                        return "info";
                    }
                }));
        finish();
    }
}

ExampleActivityonCreate启动了另外一个Activity:Example2Activity,之后将自身销毁(finish())不再使用, 之后是否就意味着ExampleActivity对象所占用的内存就可以被回收了呢?非也,还有对象持有对ExampleActivity的引用。是那个Intent吗? 不是,而是那个实现了Serializable接口的匿名内部类对象。在Java中,匿名内部类会持有其外部类的引用并一直保留。所以上述例子中匿名的Serializable对象会持有ExampleActivity的引用。如果第二个Activity:Example2Activity一直维持着这个匿名类对象引用,那么它也会持有ExampleActivity对象的引用。这就会造成ExampleActivity对象的泄露。

总结起来,那些不再被程序使用却又被引用着的对象都是泄露的对象。

内存泄露的危害

从程序上来说,一两次内存泄露并不会对程序造成致命的威胁。用户一般也感觉不到内存泄露的存在。真正有危害的是内存泄漏的堆积,大量的内存泄露会迅速消耗可用内存,初期会导致GC操作频繁进行,进而导致程序运行性能下降。最终则会导致内存溢出(Out of Memory)

内存泄露与内存溢出

内存溢出(OOM)是指程序在申请内存时,没有足够的内存空间供其使用。通俗地讲就是程序正常运行所需的内存超过了系统给定的限制。就像一个盘子最多装4个苹果,你硬要塞进去5个,结果有一个掉地上了,这就是溢出。
大量积累的内存泄露最终会导致内存溢出。但并不是所有内存溢出都是内存泄露导致的。

Android中常见的内存泄露场景与解决方法

1. 非静态内部类的静态变量比较容易产生内存泄露。

由于非静态内部类的实例都会持有外部类对象的引用,当这个实例被声明为static时,则比较容易产生内存泄露。因为静态成员变量存在时间一般都比较长。我们看下面的一个例子:

public class MainActivity extends Activity {
    static Demo sInstance = null;

    @Override
    public void onCreate(BundlesavedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (sInstance == null) {
           sInstance= new Demo();
        }
    }
    class Demo {
        void doSomething() {
            System.out.print("dosth.");
        }
    }
}

上面的代码中的sInstance属性被声明为静态变量,当MainActivity 实例act1创建时,sInstance会获得并一直持有act1的引用。当MainAcitivity销毁后重建(例如旋转屏幕),因为sInstance持有act1的引用,所以act1是无法被GC回收的。这时进程中会存在2个MainActivity实例(act1和重建后的MainActivity实例),这个act1对象就是一个无用但一直占用着内存的对象,即无法回收的垃圾对象。解决上述问题的方法是将内部类Demo声明为static。

2. Handler 造成内存泄露

先看如下代码:

public class SampleActivity extends Activity {
    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // do something
        }
    }
	
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 发送一个10分钟后执行的一个消息
        mHandler.postDelayed(new Runnable() {
          @Override
          public void run() { }
        }, 600000);

        // 结束当前的Activity
        finish();
}

上述代码中因Handler导致内存泄露的原因是:
- 当Android应用启动的时候,会先创建一个应用主线程的Looper对象,Looper实现了一个简单的消息队列,一个一个的处理里面的Message对象。主线程Looper对象在整个应用生命周期中存在。
- 当在主线程中初始化Handler时,该HandlerLooper的消息队列关联。发送到消息队列的Message会引用发送该消息的Handler对象,这样系统可以调用Handler.handleMessage(Message)来分发处理该消息。
- 在Java中,非静态(匿名)内部类会引用外部类对象。而静态内部类不会引用外部类对象。
- 如果外部类是Activity,则会引起Activity泄露(MessageQueue -> Message -> Handler -> Activity)。
解决的办法有两个:
(1) 在使用Activity生命周期结束前清除掉MessageQueue中的发送给HandlerMessage对象(例如在ActivityonDestroy()方法中调用HandlerremoveMessagesremoveMessages方法);
(2) 将Handler的实现类改成静态内部类的方式,避免对外部类的强引用,在其内部声明一个WeakReference引用到外部类的实例。详见:http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html

3. Thread 造成内存泄露

还是先上代码:

public class MainActivity extends Activity {

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

  private void exampleOne() {
    new Thread() {
      @Override
      public void run() {
        while (true) {
          SystemClock.sleep(1000);
        }
      }
    }.start();
  }
}

代码中,Activity在启动后开启了一个线程,并循环执行该线程中的任务。当MainActivity被销毁之后,匿名内部类Thread仍持有对Activity的隐式引用,从而导致内存泄露。这种情况同样也适用于使用AsyncTask的情况。
解决办法也是将匿名内部类线程改成静态内部类。
详见:http://www.androiddesignpatterns.com/2013/04/activitys-threads-memory-leaks.html

上述几个例子所描述的内存泄露场景有个共同的特点,就是都是使用了非静态内部类。的确,使用非静态内部类写起程序来似乎更加顺手,但是使用非静态内部类一不小心就可能会造成外部类内存泄露,尤其当外部类是Activity时,内存泄露可能会比较严重(也就是我们常听说的Context泄露),因为Activity对象一般都会引用大量的其他对象。

4. 调用registerReceiver后未调用unregisterReceiver()

registerReceiver(new BroadcastReceiver() {  
    ...  
}, filter);

在调用registerReceiver后,若未调用unregisterReceiver,其所占的内存是相当大的。这种情况同样适用于注册一个Listener的情况。
例如我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。
但是如果在释放LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被GC回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OOM,使得system_process进程挂掉。

5. 集合中对象没清理

当然这里的集合泛指各种数组、ListMapSet等。本文中最初的例子就是属于这种情况。这种场景的内存泄露不太容易被察觉,所以需要特别小心。解决办法也很简单,对于上面的例子来说,只需要修改pop函数中return语句为:

Obj obj = objectPool[pointer];
	objectPool[pointer] = null;
	--pointer;
	return obj;

6. 资源对象没关闭

资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于Java虚拟机内,还存在于Java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄露。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该立即调用它的close()函数,将其关闭掉,然后再置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。
例如,使用Cursor的最佳实践:

Cursor cursor = null;
try {
  	cursor = getContentResolver().query(uri ...);
  	if (cursor != null && cursor.moveToNext()) {
  	  	// TODO
  	}
} finally {
  	if (cursor != null) {
	    try { 
	        cursor.close();
	    } catch (Exception e) {
	        //ignore this
  	  	}
  	}
}

7. Bitmap对象未释放

有时我们会手工地操作Bitmap对象,如果一个Bitmap对象不再被使用的时候,应该调用Bitmap.recycle()方法回收此对象的像素所占用的内存。
另外在最新版本的Android开发时,使用下面的方法也可以释放此Bitmap所占用的内存

Bitmap bitmap ;
 ...
 bitmap初始化以及使用
 ...
bitmap = null;

8. 单例模式中不合理使用Context.

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

如果上述代码中getInstance传入的context参数是一个Activity对象,则会造成Activity泄露,因为instance是静态的。正确的应该用context.getApplicationContext()

以上仅仅列出来一些典型的内存泄露场景,现实编程过程中稍不小心可能就会写出内存泄露的代码来,而自己却没有意识到,所以我们需要借助一些专业的分析工具来帮助我们分析找出程序中的内存泄露场景。下一篇我们将介绍2个工具帮忙我们分析内存泄露,他们分别是MATLeakCanary.

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

blog comments powered by Disqus