前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >匿名内部类导致内存泄露的面试题

匿名内部类导致内存泄露的面试题

作者头像
PhoenixZheng
发布2018-08-07 16:34:24
6.4K0
发布2018-08-07 16:34:24
举报

内存泄露一直是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

代码语言:javascript
复制
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

代码语言:javascript
复制
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,再编译一次看字节码看看

代码语言:javascript
复制
private static Runnable runnable = new Runnable(){
代码语言:javascript
复制
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,可以去掉对外部类的引用,同时能继续使用外部类的变量。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-04-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android每日一讲 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 非静态内部类
  • 为什么会持有外部类?
  • 那么非静态匿名内部类是如何持有的?
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档