内存泄露一直是Java中需要避免的问题,也是面试时经常拿来考察编程能力的题目。比如下面这个问题,
问:为什么使用非静态内部类可能导致内存泄露? 答:非静态内部类会持有外部类的引用,从而导致GC可能回收不了这部分引用,导致OOM
但具体是怎么发生OOM的?还有这里面的原理是怎样的呢?
打个比方在Android开发中最典型的例子就是Handler。 先来看一个截图,在Android开发中经常在Activity里写一个Handler实例用来处理线程通信,如果实例是非静态的,那么lint会提示这个错误
Alt text
'This handler class should be static or leaks might occur'
非静态匿名内部类会持有外部类的引用,从而导致内存泄露。可能这么说还不够清楚,举个例子,如果每次启动Acitivity就给Handler发一个耗时的Runnable,然后不停退出重进Activity,就能引发内存泄露。 因为mHandler会一直持有Activity的引用,而mHandler会一直被UI线程引用,存在引用关系的对象是不会被GC回收的。所以引用关系链上最终的Activity对象在没有被回收的情况下越来越多,就会导致OOM。
But why?
其实这是个值得思考的问题,理清这个问题也就明白匿名内部类的设计初衷了。非静态匿名内部类持有外部类这种设计的原理和作用,可以看下面的demo
public class NonStaticInnerDemo {
private static String TAG = "Outter";
private Runnable runnable = new Runnable(){
public void run(){
System.out.println("inner run: " + TAG);
}
};
}
现在我们把这个类编译一下看产出结果,
$ javac NonStaticInnerDemo.java $ ls *.class 'NonStaticInnerDemo$1.class' NonStaticInnerDemo.class
这里面$1就是匿名内部类了。非静态匿名内部类持有外部类可以总结为以下两个作用 · 当类B仅在类A内使用,匿名内部类可以让外部不知道类B的存在,从而减少代码的维护 · 类B持有类A,那么B就可以使用A中的变量,比如上面的代码,在Runnable里可以使用 NonStaticInnerDemo的 TAG
这两个作用可以解释 Why的问题。
But how?
既然 $1 是匿名内部类的 class文件,那么看它的字节码可以看明白
$ javap -c NonStaticInnerDemo\$1.class
Compiled from "NonStaticInnerDemo.java"
class NonStaticInnerDemo$1 implements java.lang.Runnable {
final NonStaticInnerDemo this$0;
NonStaticInnerDemo$1(NonStaticInnerDemo);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:LNonStaticInnerDemo;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return
public void run();
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #4 // class java/lang/StringBuilder
6: dup
7: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
10: ldc #6 // String inner run:
12: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokestatic #8 // Method NonStaticInnerDemo.access$000:()Ljava/lang/String;
18: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
}
关注字节码中的 putfield这一行,这里表示有一个对 NonStaticInnerDemo的引用被存在了 this$0 中,也就是说它持有了外部类的对象。到这里就明白了为什么非静态匿名内部类会导致内存泄露了。
那么为什么静态匿名内部类不会呢?我们把demo代码的 runnable改为static,再编译一次看字节码看看
private static Runnable runnable = new Runnable(){
Compiled from "NonStaticInnerDemo.java"
final class NonStaticInnerDemo$1 implements java.lang.Runnable {
NonStaticInnerDemo$1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void run();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String inner run:
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokestatic #7 // Method NonStaticInnerDemo.access$000:()Ljava/lang/String;
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
}
对比可以发现这里少了一行 putfield,说明对于一个静态匿名内部类来说,它不会持有外部类的引用。
既然如此,那么静态匿名内部类是如何引用外部类的变量呢? 其实很简单,因为它是静态的,所以它引用的外部类的变量也必须是静态对象,这样一来静态变量就会被存放在JVM内存模型的Method Area,从而可以直接引用到需要的变量。
Java的匿名内部类让代码更容易维护更清晰,但是非静态的内部类会持有外部类的引用,从而导致可能出现OOM。通过把内部类改为static,可以去掉对外部类的引用,同时能继续使用外部类的变量。