专栏首页Phoenix的Android之旅匿名内部类导致内存泄露的面试题

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

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

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

如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • Java中,匿名内部类在开发中的使用以及匿名内部类的面试题

      我们在开发的时候,会看到抽象类,或者接口作为方法的形式参数。   而这个时候,我们知道实际需要的是一个子类的对象。 如果该方法仅仅调用一次,我们就可以...

    黑泽君
  • Android 关于内存泄露,你必须了解的东西

    内存管理的目的就是让我们在开发过程中有效避免我们的应用程序出现内存泄露的问题。内存泄露相信大家都不陌生,我们可以这样理解:「没有用的对象无法回收的现象就是内存泄...

    developerHaoz
  • Android常见内存泄露,学会这六招大大优化APP性能

    很多开发者都知道,在面试的时候会经常被问到内存泄露和内存溢出的问题。 内存溢出(Out Of Memory,简称 OOM),通俗理解就是内存不够...

    分享达人秀
  • Android中Handler引起的内存泄露

    在Android常用编程中,Handler在进行异步操作并处理返回结果时经常被使用。通常我们的代码会这样实现。

    技术小黑屋
  • 源码分析|Handler内存泄漏分析及解决

    内存泄露是android开发者经常遇到的一个话题,除了activity的内存泄露,我们不妨看看Handler的内存泄露!

    开发者技术前线
  • IP匿名性研究

    既然不可能做到完全匿名,我们只能提高相对匿名性,从而使得自己在网络中的隐私度更高,安全性更高。

    安恒网络空间安全讲武堂
  • Android异步通信:你了解Handler内存泄露吗?

    在Android开发中,内存泄露十分常见。本文将详细讲解内存泄露的其中一种情况:在Handler中发生的内存泄露

    Carson.Ho
  • 面试官:Handler内存泄露的原因是什么?我:就这?太简单了吧,但我却被挂了...

    "内部类持有了外部类的引用,也就是Hanlder持有了Activity的引用,从而导致无法被回收呗。"

    Android技术干货分享
  • 小题大做 | Handler内存泄露全面分析

    "内部类持有了外部类的引用,也就是Hanlder持有了Activity的引用,从而导致无法被回收呗。"

    码上积木
  • Android性能优化之内存泄漏,你想要的这里都有~

    在Android中,内存泄露的现象十分常见;而内存泄露导致的后果会使得应用Crash 本文 全面介绍了内存泄露的本质、原因 & 解决方案,最终提供一些常见的内存...

    用户9239674
  • Android性能优化:手把手带你全面了解 内存泄露 & 解决方案

    Carson.Ho
  • 解决Android使用Handler造成内存泄露问题

      Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收。也就是说...

    砸漏
  • 来来来,聊聊7种内存泄露场景和13种解决方案

    Java通过垃圾回收机制,可以自动的管理内存,这对开发人员来说是多么美好的事啊。但垃圾回收器并不是万能的,它能够处理大部分场景下的内存清理、内存泄露以及内存优化...

    程序新视界
  • 详解Android使用Handler造成内存泄露的分析及解决方法

    Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收。也就是说,一...

    砸漏
  • 永远不要使用双花括号初始化实例,否则就会OOM!

    生活中的尴尬无处不在,有时候你只是想简单的装一把,但某些“老同志”总是在不经意之间,给你无情的一脚,踹得你简直无法呼吸。

    Java中文社群-磊哥
  • 应用稳定性优化系列(三),资源泄露问题分析及定位

    继介绍稳定性ANR类故障和Crash/Tombstone类故障后,本章将介绍第三大类故障资源泄露及其典型场景、分析定位和解决方法。

    软件绿色联盟
  • Android开发:详解Handler的内存泄露

    上面提到,在Java里,非静态内部类和匿名类都会潜在的引用它们所属的外部类。 但是,静态内部类不会。

    Carson.Ho

扫码关注云+社区

领取腾讯云代金券