在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是必选项哦