Java >> Java チュートリアル >  >> JVM

JVM での Invoke Dynamic の紹介

1.概要

Invoke Dynamic (Indy とも呼ばれます) は、動的型付け言語の JVM サポートを強化することを目的とした JSR 292 の一部でした。 Java 7 での最初のリリースの後、invokedynamic opcode は、JRuby などの動的な JVM ベースの言語や、Java などの静的に型付けされた言語でさえ、非常に広く使用されています。

このチュートリアルでは、invokedynamic についてわかりやすく説明します どのようにできるか見てみましょう ライブラリと言語の設計者がさまざまな形式のダイナミクスを実装するのを支援します。

2.インボーク ダイナミックに出会う

Stream API 呼び出しの単純なチェーンから始めましょう:

public class Main { 

    public static void main(String[] args) {
        long lengthyColors = List.of("Red", "Green", "Blue")
          .stream().filter(c -> c.length() > 3).count();
    }
}

最初は、Java が Predicate から派生する匿名の内部クラスを作成すると考えるかもしれません。 そのインスタンスをフィルタに渡します 方法。 しかし、私たちは間違っているでしょう.

2.1.バイトコード

この仮定を確認するために、生成されたバイトコードを見てみましょう:

javap -c -p Main
// truncated
// class names are simplified for the sake of brevity 
// for instance, Stream is actually java/util/stream/Stream
0: ldc               #7             // String Red
2: ldc               #9             // String Green
4: ldc               #11            // String Blue
6: invokestatic      #13            // InterfaceMethod List.of:(LObject;LObject;)LList;
9: invokeinterface   #19,  1        // InterfaceMethod List.stream:()LStream;
14: invokedynamic    #23,  0        // InvokeDynamic #0:test:()LPredicate;
19: invokeinterface  #27,  2        // InterfaceMethod Stream.filter:(LPredicate;)LStream;
24: invokeinterface  #33,  1        // InterfaceMethod Stream.count:()J
29: lstore_1
30: return

匿名の内部クラスはありません 確かに、そのようなクラスのインスタンスを filter に渡す人はいません メソッド.

驚くべきことに、invokedynamic 命令は、述語の作成になんらかの責任を負います インスタンス。

2.2.ラムダ固有のメソッド

さらに、Java コンパイラは、次の面白い静的メソッドも生成しました:

private static boolean lambda$main$0(java.lang.String);
    Code:
       0: aload_0
       1: invokevirtual #37                 // Method java/lang/String.length:()I
       4: iconst_3
       5: if_icmple     12
       8: iconst_1
       9: goto          13
      12: iconst_0
      13: ireturn

このメソッドは文字列を取ります 入力として、次の手順を実行します:

  • 入力長の計算 (invokevirtual 長さ )
  • 長さを定数 3 と比較 (if_icmple そして iconst_3 )
  • false を返す 長さが 3 以下の場合

興味深いことに、これは実際には filter に渡したラムダと同等です メソッド:

c -> c.length() > 3

したがって、匿名の内部クラスの代わりに、Java は特別な静的メソッドを作成し、invokedynamic を介して何らかの方法でそのメソッドを呼び出します。

この記事では、この呼び出しが内部でどのように機能するかを見ていきます。しかし、まず、invokedynamic という問題を定義しましょう。 解決しようとしています。

2.3.問題

Java 7 より前の JVM には、次の 4 つのメソッド呼び出しタイプしかありませんでした:invokevirtual 通常のクラス メソッドを呼び出すには、 invokestatic 静的メソッドを呼び出すには、invokeinterface インターフェイス メソッドを呼び出し、invokespecial コンストラクターまたはプライベート メソッドを呼び出す。

違いはありますが、これらの呼び出しはすべて 1 つの単純な特徴を共有しています。各メソッド呼び出しを完了するための事前定義された手順がいくつかあり、これらの手順を独自の動作で強化することはできません。

この制限には主に 2 つの回避策があります。1 つはコンパイル時、もう 1 つは実行時です。前者は通常、Scala や Koltin などの言語で使用され、後者は JRuby などの JVM ベースの動的言語に最適なソリューションです。

通常、ランタイム アプローチはリフレクション ベースであるため、非効率的です。

一方、コンパイル時のソリューションは通常、コンパイル時のコード生成に依存しています。このアプローチは、実行時により効率的です。ただし、これはやや脆く、処理するバイトコードが増えるため、起動時間が遅くなる可能性もあります。

問題の理解が深まったので、解決策が内部でどのように機能するかを見てみましょう。

3.ボンネットの下

動的呼び出し 任意の方法でメソッド呼び出しプロセスをブートストラップできます .つまり、JVM が invokedynamic を検出したときです。 opcode を初めて実行すると、ブートストラップ メソッドと呼ばれる特別なメソッドを呼び出して、呼び出しプロセスを初期化します。

ブートストラップ メソッドは、呼び出しプロセスを設定するために作成した通常の Java コードです。したがって、任意のロジックを含めることができます。

ブートストラップ メソッドが正常に完了すると、CallSite のインスタンスが返されます。 この CallSite 次の情報をカプセル化します:

  • JVM が実行する実際のロジックへのポインタ。これは MethodHandle として表す必要があります。
  • 返された CallSite の有効性を表す条件。

これ以降、JVM がこの特定のオペコードを再度検出するたびに、スロー パスをスキップし、基になる実行可能ファイルを直接呼び出します .さらに、JVM は、CallSite の条件が満たされるまで低速パスをスキップし続けます。

Reflection API とは対照的に、JVM は MethodHandle を完全にシースルーできます。 s とそれらを最適化しようとするため、パフォーマンスが向上します。

3.1.ブートストラップ法表

生成された invokedynamic をもう一度見てみましょう バイトコード:

14: invokedynamic #23,  0  // InvokeDynamic #0:test:()Ljava/util/function/Predicate;

これは、この特定の命令が、ブートストラップ メソッド テーブルから最初のブートストラップ メソッド(#0 の部分)を呼び出す必要があることを意味します。 また、ブートストラップ メソッドに渡すいくつかの引数についても言及しています:

  • テスト Predicate の唯一の抽象メソッドです
  • ()Ljava/util/function/述語 JVM のメソッド シグネチャを表します。メソッドは入力として何も受け取らず、述語 のインスタンスを返します。 インターフェース

ラムダの例のブートストラップ メソッド テーブルを表示するには、-v を渡す必要があります。 javap: へのオプション:

javap -c -p -v Main
// truncated
// added new lines for brevity
BootstrapMethods:
  0: #55 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:
    (Ljava/lang/invoke/MethodHandles$Lookup;
     Ljava/lang/String;
     Ljava/lang/invoke/MethodType;
     Ljava/lang/invoke/MethodType;
     Ljava/lang/invoke/MethodHandle;
     Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #62 (Ljava/lang/Object;)Z
      #64 REF_invokeStatic Main.lambda$main$0:(Ljava/lang/String;)Z
      #67 (Ljava/lang/String;)Z

すべてのラムダのブートストラップ メソッドは metafactory です LambdaMetafactory の静的メソッド クラス。

他のすべてのブートストラップ メソッドと同様に、このメソッドは次のように少なくとも 3 つの引数を取ります :

  • Ljava/lang/invoke/MethodHandles$Lookup 引数は、invokedynamic のルックアップ コンテキストを表します
  • Ljava/lang/String 呼び出しサイトのメソッド名を表します。この例では、メソッド名は test です
  • Ljava/lang/invoke/MethodType 呼び出しサイトの動的メソッド シグネチャです。この場合は、()Ljava/util/function/Predicate です。

これら 3 つの引数に加えて、ブートストラップ メソッドはオプションで 1 つ以上の追加パラメータを受け入れることもできます。 この例では、これらは余分なものです:

  • (Ljava/lang/Object;)Z Object のインスタンスを受け入れる、消去されたメソッド シグネチャです。 ブール値を返します。
  • REF_invokeStatic Main.lambda$main$0:(Ljava/lang/String;)Z メソッドハンドルです 実際のラムダ ロジックを指しています。
  • (Ljava/lang/String;)Z 文字列を 1 つ受け入れる、消去されていないメソッド シグネチャです。 ブール値を返します。

簡単に言えば、JVM は必要なすべての情報をブートストラップ メソッドに渡します。 Bootstrap メソッドは、その情報を使用して Predicate の適切なインスタンスを作成します。 次に、JVM はそのインスタンスを filter に渡します。 メソッド。

3.2.さまざまなタイプのCallSite

JVM が invokedynamic を認識したら この例では、初めてブートストラップ メソッドを呼び出します。この記事を書いている時点で、ラムダ ブートストラップ メソッドは InnerClassLambdaMetafactory 実行時にラムダの内部クラスを生成します。

次に、ブートストラップ メソッドは、生成された内部クラスを特別なタイプの CallSite 内にカプセル化します。 ConstantCallSite として知られています。 このタイプのCallSite セットアップ後は変更されません。 したがって、各ラムダの最初のセットアップの後、JVM は常にファスト パスを使用してラムダ ロジックを直接呼び出します。

これは最も効率的な invokedynamic のタイプですが、 利用可能なオプションはこれだけではありません。実際のところ、Java は MutableCallSite を提供します。 および VolatileCallSite より動的な要件に対応します。

3.3.利点

したがって、ラムダ式を実装するために、コンパイル時に匿名の内部クラスを作成する代わりに、Java は invokedynamic を介して実行時にそれらを作成します。

内部クラスの生成を実行時まで遅らせることに反対する人もいるかもしれません。ただし、invokedynamic このアプローチには、単純なコンパイル時のソリューションよりもいくつかの利点があります。

まず、JVM は最初にラムダを使用するまで内部クラスを生成しません。したがって、最初のラムダ実行の前に内部クラスに関連する余分なフットプリントを支払うことはありません .

さらに、リンケージ ロジックの多くがバイトコードからブートストラップ メソッドに移動されています。したがって、invokedynamic バイトコードは通常、代替ソリューションよりもはるかに小さい .バイトコードが小さいほど、起動速度が向上します。

Java の新しいバージョンに、より効率的なブートストラップ メソッドの実装が付属しているとします。 次に、invokedynamic バイトコードは、再コンパイルせずにこの改善を利用できます .このようにして、ある種の転送バイナリ互換性を実現できます。基本的に、再コンパイルせずに異なる戦略を切り替えることができます。

最後に、Java でブートストラップとリンケージ ロジックを記述することは、通常、AST をトラバースして複雑なバイトコードを生成するよりも簡単です。つまり、invokedynamic (主観的に) 壊れにくくなる可能性があります。

4.その他の例

ラムダ式だけが機能というわけではなく、invokedynamic を使用する言語は Java だけではありません。 このセクションでは、動的呼び出しの他のいくつかの例に慣れていきます。

4.1. Java 14:レコード

レコードは、Java 14 の新しいプレビュー機能であり、ダム データ ホルダーであるはずのクラスを宣言するための簡潔な構文を提供します。

以下は簡単なレコードの例です:

public record Color(String name, int code) {}

この単純なワンライナーを指定すると、Java コンパイラはアクセサ メソッドの適切な実装を生成します。toString、equals、 そしてハッシュコード。

toString、equals を実装するには またはハッシュコード Java は を使用しています invokedynamic . たとえば、 equals のバイトコード

public final boolean equals(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: invokedynamic #27,  0  // InvokeDynamic #0:equals:(LColor;Ljava/lang/Object;)Z
       7: ireturn

別の解決策は、すべてのレコード フィールドを検索し、equals を生成することです。 コンパイル時にこれらのフィールドに基づくロジック。 フィールドが多ければ多いほど、バイトコードは長くなります .

反対に、Java はブートストラップ メソッドを呼び出して、実行時に適切な実装をリンクします。したがって、フィールド数に関係なく、バイトコードの長さは一定のままです。

バイトコードを詳しく見ると、ブートストラップ メソッドは ObjectMethods#bootstrap であることがわかります :

BootstrapMethods:
  0: #42 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:
    (Ljava/lang/invoke/MethodHandles$Lookup;
     Ljava/lang/String;
     Ljava/lang/invoke/TypeDescriptor;
     Ljava/lang/Class;
     Ljava/lang/String;
     [Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
    Method arguments:
      #8 Color
      #49 name;code
      #51 REF_getField Color.name:Ljava/lang/String;
      #52 REF_getField Color.code:I

4.2. Java 9:​​ 文字列連結

Java 9 より前は、重要な文字列連結は StringBuilder を使用して実装されていました。 JEP 280 の一部として、文字列の連結に invokedynamic が使用されるようになりました。 たとえば、定数文字列を確率変数と連結してみましょう:

"random-" + ThreadLocalRandom.current().nextInt();

この例のバイトコードは次のようになります:

0: invokestatic  #7          // Method ThreadLocalRandom.current:()LThreadLocalRandom;
3: invokevirtual #13         // Method ThreadLocalRandom.nextInt:()I
6: invokedynamic #17,  0     // InvokeDynamic #0:makeConcatWithConstants:(I)LString;

さらに、文字列連結のブートストラップ メソッドは StringConcatFactory にあります。 クラス:

BootstrapMethods:
  0: #30 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:
    (Ljava/lang/invoke/MethodHandles$Lookup;
     Ljava/lang/String;
     Ljava/lang/invoke/MethodType;
     Ljava/lang/String;
     [Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #36 random-\u0001

5.結論

この記事では、まずインディーズが解決しようとしている問題について理解しました。

次に、単純なラムダ式の例を見ていくことで、invokedynamic の方法を確認しました。 内部で動作します。

最後に、Java の最近のバージョンにおける indy の他の例をいくつか列挙しました。


Java タグ