内存泄露一直是Java开发中需要避免的问题,也是面试时经常考察的问题。
使用非静态内部类是日常开发中最容易产生内存泄露的场景,本文主要探讨为什么使用非静态内部类可能产生内存泄露以及如何解决此类问题。
非静态内部类持有外部类的引用
要理解为什么使用非静态内部类可能产生内存泄露,首先要知道非静态内部类是隐式持有外部类引用的。看下面这个Outter类,它有一个非静态内部类Inner:
public class Outter {
private static String TAG = "Outter";
private class Inner {
public Inner() {
System.out.println("Inner Constructor: " + TAG);
}
}
}
现在我们把这个类编译一下
$ javac Outter.java
$ ls *.class
Outter$Inner.class Outter.class
编译产物中Outter$Inner.class
就是这个非静态内部类的字节码文件,接下来我们分析一下它:
$ javap -c Outter$Inner.class
Compiled from "Outter.java"
class Outter$Inner {
final Outter this$0;
public Outter$Inner(Outter);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:LOutter;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
12: new #4 // class java/lang/StringBuilder
15: dup
16: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
19: ldc #6 // String Inner Constructor:
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokestatic #8 // Method Outter.access$000:()Ljava/lang/String;
27: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
30: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
33: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: return
}
关注字节码中的putfield
这一行,表示将外部类Outter对象的引用赋值给了匿名内部类中的this$0
,即非静态内部类持有了外部类的引用。
通过同样的方式分析静态内部类的字节码文件,我们可以发现静态内部类并不持有外部类的引用,所以它只能引用的外部类的静态成员,因为静态成员被存放在JVM内存模型的Method Area,可以直接被引用到。
Android Handler可能导致的内存泄露
Android开发中产生内存泄露的最典型场景就是在Activity里创建一个非静态Handler实例来处理线程间通信:
上图定义的继承于Handler的匿名内部类是非静态的,尽管它内部并未显式持有任何对象的引用,lint依然提示如下警告:
This Handler class should be static or leaks might occur
意思就是这个继承于Handler的匿名内部类应该被定义为static,否则可能产生内存泄露。
通过前面的介绍我们知道,非静态内部类会持有外部类的引用,因此上述继承于Handler的匿名内部类隐式持有了Activity对象的引用,如果此时通过mHandler.sendMessageDelayed(new Message(), 5000);
延时5s发送消息,然后立刻退出Activity,这5s内Activity对象将无法被GC回收,也就出现了内存泄露。
此外,下面这种用法也会造成内存泄露,因为实现了Runnable接口的匿名内部类持有外部类的引用:
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
// do something
}
}, 5000);
那么如何解决这种内存泄露呢?参考lint给我们的提示,我们需要把上述用到的内部类定义成static,这样它就不再隐式持有外部类的引用;除此之外,如果内部类需要显式持有外部类成员引用以完成某种操作,必须通过弱引用(WeakReference)实现,这样当这些外部类成员所依赖的上下文被回收时,由于只具有弱引用,GC工作时就会将他们回收,从而避免了内存泄露。
特别的,针对Activity中创建非静态Handler处理线程间通信导致内存泄露的场景,更推荐在onDestroy()中调用mHandler.removeCallbacksAndMessages(null)
解决