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

内存泄露一直是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,可以去掉对外部类的引用,同时能继续使用外部类的变量。

本文分享自微信公众号 - Android每日一讲(gh_f053f29083b9)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-04-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏精讲JAVA

揭开Java 泛型类型擦除神秘面纱

大家可能会有疑问,我为什么叫做泛型是一个守门者。这其实是我个人的看法而已,我的意思是说泛型没有其看起来那么深不可测,它并不神秘与神奇。泛型是 Java 中一个很...

14610
来自专栏用户画像

Maven配置本地环境和线上环境

有时候,我们在开发和部署的时候,有很多配置文件的数据是不一样的,比如数据库的properties文件等等每次部署或者开发都要改配置文件太麻烦了,这个时候,就需要...

11040
来自专栏微信公众号:Java团长

Java泛型,你了解类型擦除吗?

大家可能会有疑问,我为什么叫做泛型是一个守门者。这其实是我个人的看法而已,我的意思是说泛型没有其看起来那么深不可测,它并不神秘与神奇。泛型是 Java 中一个很...

16720
来自专栏HTML5学堂

JavaScript | 函数定义的两种方法;预编译与执行

HTML5学堂(码匠):在JavaScript当中,函数的定义有两种常见方法,这两种方法有何不同?与这种不同点息息相关的“预编译与执行”又是什么意思? 1.如...

38580
来自专栏Java架构师历程

精选30道Java笔试题解答

都是一些非常非常基础的题,是我最近参加各大IT公司笔试后靠记忆记下来的,经过整理献给与我一样参加各大IT校园招聘的同学们,纯考Java基础功底,老手们就不用进来...

38340
来自专栏SpringBoot

javabean 遍历

16020
来自专栏Ryan Miao

String的内存模型,为什么String被设计成不可变的

String是Java中最常用的类,是不可变的(Immutable), 那么String是如何实现Immutable呢,String为什么要设计成不可变呢? 前...

387130
来自专栏实战docker

Java虚拟机学习:方法调用的字节码指令

我们在写java程序的时候会进行各种方法调用,虚拟机在执行这些调用的时候会用到不同的字节码指令,共有如下五种: 1. invokespecial:调用私有实...

222100
来自专栏Java架构沉思录

关于泛型,你可能不知道的事儿

大家可能会有疑问,我为什么叫做泛型是一个守门者。这其实是我个人的看法而已,我的意思是说泛型没有其看起来那么深不可测,它并不神秘与神奇。泛型是 Java 中一个很...

12410
来自专栏Java帮帮-微信公众号-技术文章全总结

Java面试系列4

Java面试系列4 一、一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 可以。必须只有一个类名与文件名相同。Public的类必须和文件...

36760

扫码关注云+社区

领取腾讯云代金券