匿名内部类为什么泄漏?Lambda为什么不泄漏?

作者:麦客奥德彪

在Android开发中,内存泄露发生的场景其实主要就两点,一是数据过大的问题,而是调用与被调用生命周期不一致问题,对于对象生命周期不一致导致的泄漏问题占90%,最常见的也不好分析的当属匿名内部类的内存泄漏。

最近在开发时遇到了一个问题,就是LeakCannry 检测到的内存泄漏,LeakCannry检测的原理大概就是GC 可达性算法实现的,我们产品中最多的一个问题就是匿名内部类导致的。

案例不涉及持有外部类引用的状态下

匿名内部类如何导致内存泄漏

在Java体系中,内部类有多种,最常见的就是静态内部类、匿名内部类,一般情况下,都推荐使用静态内部类,那这是为什么呢,先看一个例子:

public class Test {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {

            }
        }).start();
    }
}

匿名内部类的泄漏原因:内部类持有外部类的引用,上述场景中,当外部类销毁时,匿名内部类Runnable 会导致内存泄漏,

验证这个结论

上述代码的class 文件通过Javap -c 查看后是这样的

Compiled from "Test.java"
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/Thread
       3: dup
       4: new           #3                  // class Test$1
       7: dup
       8: invokespecial #4                  // Method Test$1."<init>":()V
      11: invokespecial #5                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      14: invokevirtual #6                  // Method java/lang/Thread.start:()V
      17: return
}

我们直接看main 方法中的指令:

0: new #2 // 创建一个新的 Thread 对象 
3: dup // 复制栈顶的对象引用 
4: new #3 // 创建一个匿名内部类 Test$1 的实例 
7: dup // 复制栈顶的对象引用 
8: invokespecial #4 // 调用匿名内部类 Test$1 的构造方法 
11: invokespecial #5 // 调用 Thread 类的构造方法,传入匿名内部类对象 
14: invokevirtual #6 // 调用 Thread 类的 start 方法,启动线程 
17: return // 返回

我们可以看到,在第4步中 使用new 指令创建了一个Test$1的实例,并且在第8步中,通过invokespecial 指令调用匿名内部类的构造方法,这样一来生成的内部类就会持有外部类的引用,从而外部类不能回收,将导致内存泄漏。

Lambda为什么不泄漏

刚开始,我以为Lambda只是语法糖,不会有其他的作用,然而,哈哈 大家估计已经想到了,

匿名内部类使用Lambda 时不会造成内存泄漏。

看代码:

public class Test {
    public static void main(String[] args) {
        new Thread(() -> {

        }).start();
    }
}

将上面的代码改为Lambda 格式

class 文件:

Compiled from "Test.java"
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/Thread
       3: dup
       4: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
       9: invokespecial #4                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      12: invokevirtual #5                  // Method java/lang/Thread.start:()V
      15: return
}

第一眼看上去就已经知道了答案,在这份字节码中没有生成内部类,

在Lambda格式中,没有生成内部类,而是直接使用invokedynamic 指令动态调用run方法,生成一个Runnable对象。再调用调用Thread类的构造方法,将生成的Runnable对象传入。从而避免了持有外部类的引用,也就避免了内存泄漏的发生。

在开发中,了解字节码知识还是非常有必要的,在关键时刻,我们查看字节码,确实能帮助自己解答一些疑惑,下面是常见的一些字节码指令

常见的字节码指令

Java 字节码指令是一组在 Java 虚拟机中执行的操作码,用于执行特定的计算、加载、存储、控制流等操作。以下是 Java 字节码指令的一些常见指令及其功能:

  1. 加载和存储指令:
  • aload:从局部变量表中加载引用类型到操作数栈。
  • astore:将引用类型存储到局部变量表中。
  • iload:从局部变量表中加载 int 类型到操作数栈。
  • istore:将 int 类型存储到局部变量表中。
  • fload:从局部变量表中加载 float 类型到操作数栈。
  • fstore:将 float 类型存储到局部变量表中。
  1. 算术和逻辑指令:
  • iadd:将栈顶两个 int 类型数值相加。
  • isub:将栈顶两个 int 类型数值相减。
  • imul:将栈顶两个 int 类型数值相乘。
  • idiv:将栈顶两个 int 类型数值相除。
  • iand:将栈顶两个 int 类型数值进行按位与操作。
  • ior:将栈顶两个 int 类型数值进行按位或操作。
  1. 类型转换指令:
  • i2l:将 int 类型转换为 long 类型。
  • l2i:将 long 类型转换为 int 类型。
  • f2d:将 float 类型转换为 double 类型。
  • d2i:将 double 类型转换为 int 类型。
  1. 控制流指令:
  • if_icmpeq:如果两个 int 类型数值相等,则跳转到指定位置。
  • goto:无条件跳转到指定位置。
  • tableswitch:根据索引值跳转到不同位置的指令。
  1. 方法调用和返回指令:
  • invokevirtual:调用实例方法。
  • invokestatic:调用静态方法。
  • invokeinterface:调用接口方法。
  • ireturn:从方法中返回 int 类型值。
  • invokedynamic: 运行时动态解析并绑定方法调用

详细的字节码指令列表和说明可参考 Java 虚拟机规范(Java Virtual Machine Specification)

总结

为了解决问题而储备知识,是最快的学习方式。

在开发中,也不要刻意去设计invokedynamic的代码,但是Java开发的同学,Lambda是必选项哦

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容