JVMでのInvokeDynamicの概要
1. 概要
Invoke Dynamic(Indyとも呼ばれます)は、動的型付けされた言語のJVMサポートを強化することを目的とした JSR292の一部でした。 Java 7での最初のリリース以降、 invokedynamic オペコードは、JRubyなどの動的JVMベースの言語や、Javaなどの静的型付け言語でも非常に広く使用されています。
このチュートリアルでは、 invokedynamic をわかりやすく説明し、ライブラリと言語の設計者がさまざまな形式の動的性を実装するのにどのように役立つかを確認します。
2. InvokeDynamicに会う
StreamAPI呼び出しの単純なチェーンから始めましょう。
public class Main {
public static void main(String[] args) {
long lengthyColors = List.of("Red", "Green", "Blue")
.stream().filter(c -> c.length() > 3).count();
}
}
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
私たちが思ったことにもかかわらず、 匿名の内部クラスはありませんそして確かに、誰もそのようなクラスのインスタンスをフィルター方法
驚いたことに、 invokedynamic 命令は、Predicateインスタンスの作成に何らかの形で関与しています。
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
このメソッドは、文字列を入力として受け取り、次の手順を実行します。
- 入力長の計算( lengthでinvokevirtual)
- 長さを定数3と比較します(if_icmpleおよびiconst_3)
- 長さが3以下の場合、falseを返します
興味深いことに、これは実際にはfilterメソッドに渡したラムダと同等です。
c -> c.length() > 3
したがって、匿名の内部クラスの代わりに、Javaは特別な静的メソッドを作成し、何らかの方法でそのメソッドを呼び出します。
この記事の過程で、この呼び出しが内部でどのように機能するかを見ていきます。 しかし、最初に、invokedynamicが解決しようとしている問題を定義しましょう。
2.3. 問題
Java 7より前では、JVMには4つのメソッド呼び出しタイプしかありませんでした。通常のクラスメソッドを呼び出す invokevirtual 、静的メソッドを呼び出す invokestatic 、インターフェースメソッドを呼び出すinvokeinterface。およびinvokespecialを使用して、コンストラクターまたはプライベートメソッドを呼び出します。
それらの違いにもかかわらず、これらの呼び出しはすべて1つの単純な特性を共有しています。つまり、各メソッド呼び出しを完了するための事前定義されたステップがいくつかあり、カスタム動作でこれらのステップを強化することはできません。
この制限には、主に2つの回避策があります。1つはコンパイル時、もう1つは実行時です。 前者は通常、ScalaやKoltinなどの言語で使用され、後者はJRubyなどのJVMベースの動的言語に最適なソリューションです。
ランタイムアプローチは通常、リフレクションベースであるため、非効率的です。
一方、コンパイル時のソリューションは通常、コンパイル時のコード生成に依存しています。 このアプローチは、実行時に効率的です。 ただし、これはやや脆弱であり、処理するバイトコードが多いため、起動時間が遅くなる可能性があります。
問題の理解が深まったので、ソリューションが内部でどのように機能するかを見てみましょう。
3. フードの下
invokedynamicを使用すると、メソッド呼び出しプロセスを任意の方法でブートストラップできます。 つまり、JVMが invokedynamic opcodeを初めて検出すると、ブートストラップメソッドと呼ばれる特別なメソッドを呼び出して、呼び出しプロセスを初期化します。
ブートストラップメソッドは、呼び出しプロセスを設定するために作成したJavaコードの通常の部分です。 したがって、任意のロジックを含めることができます。
ブートストラップメソッドが正常に完了すると、次のインスタンスを返す必要があります。
- JVMが実行する必要のある実際のロジックへのポインター。 これは、
MethodHandle。 - 返されたCallSite。の有効性を表す条件
これ以降、JVMはこの特定のオペコードを再度検出するたびに、低速パスをスキップし、基盤となる実行可能ファイルを直接呼び出します。 さらに、JVMは、 CallSite の条件が変更されるまで、低速パスをスキップし続けます。
Reflection APIとは対照的に、JVMは MethodHandle を完全にシースルーし、それらを最適化しようとするため、パフォーマンスが向上します。
3.1. ブートストラップ法の表
生成されたinvokedynamicバイトコードをもう一度見てみましょう。
14: invokedynamic #23, 0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
- テストは、述語の唯一の抽象メソッドです。
- ()Ljava / util / function / Predicate は、JVMのメソッドシグネチャを表します。メソッドは入力として何も受け取らず、Predicateインターフェイスのインスタンスを返します。
ラムダの例のブートストラップメソッドテーブルを表示するには、 -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
すべてのラムダのブートストラップメソッドは、LambdaMetafactoryクラスのmetafactorystaticメソッドです。
他のすべてのブートストラップ法と同様に、これは次のように少なくとも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 は、[X91X]オブジェクトのインスタンスを受け入れ、ブール値を返す消去されたメソッドシグネチャです。
- REF_invokeStatic Main.lambda $ main $ 0:(Ljava / lang / String;)Z は、実際のラムダロジックを指すMethodHandleです。
- (Ljava / lang / String;)Z は、1つの文字列を受け入れ、ブール値を返す非消去メソッドシグネチャです。
3.2. さまざまなタイプのCallSite
この例でJVMがinvokedynamicを初めて検出すると、JVMはブートストラップメソッドを呼び出します。 この記事を書いている時点で、ラムダブートストラップメソッドは InnerClassLambdaMetafactory を使用して、実行時にラムダの内部クラスを生成します。
次に、ブートストラップメソッドは、生成された内部クラスを特別なタイプの内部にカプセル化します。 CallSite として知られている
これはinvokedynamicの最も効率的なタイプですが、これが唯一の利用可能なオプションではありません。 実際のところ、Javaは、より動的な要件に対応するためにMutableCallSiteおよびVolatileCallSiteを提供しています。
3.3. 利点
したがって、ラムダ式を実装するために、コンパイル時に匿名内部クラスを作成する代わりに、Javaは実行時にinvokedynamicを介してそれらを作成します。
内部クラスの生成を実行時まで延期することに反対する人もいるかもしれません。 ただし、 invokedynamic アプローチには、単純なコンパイル時ソリューションに比べていくつかの利点があります。
まず、JVMは、ラムダを最初に使用するまで内部クラスを生成しません。 したがって、最初のラムダ実行の前に、内部クラスに関連付けられた余分なフットプリントの料金を支払うことはありません。
さらに、リンケージロジックの多くはバイトコードからブートストラップメソッドに移動されます。 したがって、 invokedynamicバイトコードは通常、代替ソリューションよりもはるかに小さくなります。 バイトコードを小さくすると、起動速度を上げることができます。
新しいバージョンのJavaに、より効率的なブートストラップメソッドの実装が付属しているとします。 次に、invokedynamicバイトコードは再コンパイルせずにこの改善を利用できます。 このようにして、ある種の転送バイナリ互換性を実現できます。 基本的に、再コンパイルせずに異なる戦略を切り替えることができます。
最後に、Javaでブートストラップとリンケージロジックを作成することは、通常、ASTをトラバースして複雑なバイトコードを生成するよりも簡単です。 したがって、 invokedynamic は、(主観的に)脆弱性が低くなる可能性があります。
4. その他の例
ラムダ式だけが機能ではなく、Javaが使用している唯一の言語ではありません
4.1. Java 14:レコード
Records は、 Java 14 の新しいプレビュー機能であり、ダムデータホルダーであると想定されるクラスを宣言するための簡潔な構文を提供します。
簡単なレコードの例を次に示します。
public record Color(String name, int code) {}
この単純なワンライナーを考えると、Javaコンパイラはアクセサメソッドの適切な実装を生成します。 toString、等しい、 と
toString、equals、またはハッシュコードを実装するために、Javaは
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
別の解決策は、すべてのレコードフィールドを検索し、コンパイル時にそれらのフィールドに基づいて等しいロジックを生成することです。 フィールドが多いほど、バイトコードが長くなります。
それどころか、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より前は、重要な文字列の連結は、
"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の他の例をいくつか列挙しました。