なぜ匿名内部クラスはリークするのか、なぜラムダはリークしないのか

Android 開発では、実際にメモリ リークが発生する主なシナリオは 2 つあります。1 つはデータが大きすぎる問題ですが、呼び出し元と呼び出されたライフ サイクルの不一致の問題です。不一致のオブジェクト ライフ サイクルによって引き起こされるリークが 90% を占めます。最も一般的な解析が容易ではないのは、匿名内部クラスのメモリ リークです。おそらく「#メモリ リーク大集合: Android 開発者が見逃せないパフォーマンス最適化スキル」という記事にまとめました。開発中に発生した問題です。LeakCannry によって検出されたメモリ リークです。LeakCannry 検出の原理は、おそらく GC 到達可能性アルゴリズムによって実現されています。当社の製品で最も一般的な問題の 1 つは、匿名の内部クラスによって引き起こされます。

外部クラス参照を保持するステートが関与しないケース

匿名内部クラスがメモリ リークを引き起こす仕組み

Java システムには、さまざまな種類の内部クラスがあります。最も一般的なものは、静的内部クラスと匿名内部クラスです。一般に、静的内部クラスを使用することが推奨されます。なぜですか? まず例を見てみましょう:

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

            }
        }).start();
    }
}

匿名内部クラスのリークの理由: 内部クラスは外部クラスへの参照を保持しています。上記のシナリオでは、外部クラスが破棄されると、匿名内部クラス Runnable によってメモリ リークが発生します。

この結論を検証してください

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 で新しい命令を使用して Test$1 のインスタンスが作成され、ステップ 8 で匿名内部クラスのコンストラクターが invokespecial 命令を通じて呼び出され、生成された内部クラスが、外部クラスを再利用できないようにすると、メモリ リークが発生します。

Lambda がリークしない理由

最初は、Lambda は単なる糖衣構文で、他の機能は持たないだろうと思っていましたが、笑、おそらく誰もがすでに考えていたでしょう。

Lambda を使用する場合、匿名の内部クラスはメモリ リークを引き起こしません。

コードを見てください:

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

        }).start();
    }
}

上記のコードをLambda形式に変更します。

クラスファイル:

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 が必須です。

おすすめ

転載: juejin.im/post/7244002037192081468