class: center, middle # InvokeDynamic, # Under the Hood YujiSoftware Yuichi Sakuraba --- class: center, middle # javap
してますか?
--- # javap - クラスファイル解析ツール - クラスやメソッドの情報抽出 - バイトコード解析 --- ```Java public class Foo { public static void main(String... args) { System.out.println("ABC" + "DEF"); } } ``` コンパイルして、javapしてみると... -- ```console > javac Foo.java > javap Foo Compiled from "Foo.java" public class Foo { public Foo(); public static void main(java.lang.String...); } ``` --- ```console > javap -c Foo -c: 逆コンパイルオプション Compiled from "Foo.java" public class Foo { public Foo(); Code: 0: aload_0 1: invokespecial #1 // Method "init":()V 4: return public static void main(java.lang.String...); Code: 0: getstatic #7 // Field System.out 3: ldc #13 // String ABCDEF 5: invokevirtual #15 // Method println:(LString;)V 8: return } ``` ---
コンパイル時にリテラル連結
```console > javap -c Foo -c: 逆コンパイルオプション Compiled from "Foo.java" public class Foo { public Foo(); Code: 0: aload_0 1: invokespecial #1 // Method "init":()V 4: return public static void main(java.lang.String...); Code: 0: getstatic #7 * 3: ldc #13 // String ABCDEF 5: invokevirtual #15 // Method println:(LString;)V 8: return } ``` ---
invokeXXXでメソッドコール
```console > javap -c Foo -c: 逆コンパイルオプション Compiled from "Foo.java" public class Foo { public Foo(); Code: 0: aload_0 * 1: invokespecial #1 // Method "init":()V 4: return public static void main(java.lang.String...); Code: 0: getstatic #7 // Field System.out 3: ldc #13 // String ABCDEF * 5: invokevirtual #15 // Method println:(LString;)V 8: return } ``` --- では、これでは ```Java public class Foo { public static void main(String... args) { var x = "ABC"; var y = "DEF"; System.out.println(x + y); } } ``` --- ```console > javap -c Foo ... public static void main(java.lang.String...); Code: 0: ldc #7 // String ABC 2: astore_1 3: ldc #7 // String DEF 5: astore_2 6: getstatic #9 // Field System.out 9: aload_1 10: aload_2 * 11: invokedynamic #15, 0 16: invokevirtual #19 // Method println:(LString;)V 19: return } ``` --- class: center ### ### ### メソッドコールをしていないのに ### invokeで始まるバイトコード??? -- # 本日の主役: invokeDynamic --- ## invokeDynamic - あとから追加された唯一のバイトコード (Java 7) -- - メソッドコール用バイトコード - invokeVirtual: インスタンスメソッド - invokeStatic: クラスメソッド - invokeInterface: インタフェース定義メソッド - invokeSpecial: コンストラクタなど -- - **invokeDynamic: 動的メソッドディスパッチ** --
どういうこと?
--- ## 時はさかのぼって... - 2000年代初頭: JVM言語の興隆 --
JRuby
Jython
Scala
Rhino (JavaScript)
Groovy
et al.
-- - 静的型付け言語だとバイトコードに割り当てやすい - 問題は動的型付けの言語 --- ## 動的型付け言語におけるメソッドコール - 型が実行時にならないと決まらない - コールするメソッドも実行時にならないと決まらない
x.do(a, b);
--- ## 動的型付け言語におけるメソッドコール - 型が実行時にならないと決まらない - コールするメソッドも実行時にならないと決まらない
Foo.do(int a, int b) {...}
x.do(a, b);
xはFoo? Bar?
Bar.do(String a, String b) {...}
-- - 実行時にコールするメソッドを決定 --
動的メソッドディスパッチ
--- ## そこで動いたのが... --
JRuby作者 Charles Nutter
--- ## そこで動いたのが...
JRuby作者 Charles Nutter
John Rose (Sun, 現Oracle)
--- ## Da Vince Machine Project
- JVMをJava以外の言語にも広げることを目的 - 2007年に活動開始 -- - Project Lead: John Rose --
JSR 292:
Supporting Dynamically Typed Languageon the Java Platform
2011年 invokeDynamic 導入 (Java 7)
--- background-image: url(images/oracleauditorium2.jpg) ## ちょっと脱線: JVM Language Summit - Da Vince Machine Prj.主催 **JVM**特化のカンファレンス - 2008年から毎年開催 - 当初は **JVM Language** のサミット - 現在は **JVM** と **Language** のサミット --- background-image: url(images/oracleauditorium2.jpg) ## ちょっと脱線: JVM Language Summit - Da Vince Machine Prj.主催 **JVM**特化のカンファレンス - 2008年から毎年開催 - 当初は **JVM Language** のサミット - 現在は **JVM** と **Language** のサミット - 2024年は 8月4日から3日間
Oracleサンタクララキャンパスで開催 ---
---
---
--- class: center, middle # invokeDynamic の動作 --- class: center, middle #
invoke
↓ メソッドを**実行**する --- class: center, middle #
dynamic
↓ **動的に** --- # invokeDynamic とは 対象のメソッドを
動的に決定して
、実行する --
↓
どうやって決定する?
--- # invokeDynamic の処理の流れ **Bootstrapメソッド** … 実行するメソッドを決定する処理 **Lookupクラス**
…
メソッドを検索するためのクラス **CallSiteクラス**
…
実行対象のMethodHandleを保持 **MethodHandleクラス** … メソッドの参照
--- # invokeDynamic のポイント 初回のメソッド実行前に **Bootstarap を実行し**、 **
任意の処理
**をして CallSite を返す --
↓
動的メソッド検索以外のことをしても構わない --
↓
Bootstrap で**メソッドを作って返す**こともできる --
↓
**Java でも使いたい!** --- # Java と invokeDynamic Java のいろいろなところで invokeDynamic が使われている! * ラムダ式 * 文字列結合 * レコードクラス * switch 式/文のパターン・マッチング (Java 22 時点) --- class: center, middle # ラムダ式と invokeDynamic --- # ラムダ式と invokeDynamic * ラムダ式とは * 関数を簡単に書けるようにしたもの * 実体は、 関数型インタフェースの実装クラスを new すること これと invokeDynamic になんの関係が…? --- # ラムダ式と invokeDynamic * ラムダ式とは * 関数を簡単に書けるようにしたもの * 実体は、
関数型インタフェースの実装クラスを new すること
↑
**これを用意するのに invokeDynamic を使っている**
--- class: center, middle ## 実際に確認してみましょう! --- ## 検証に使用するコード * Sample クラス * isEven メソッド * リストの中身がすべて偶数か判定するメソッド ```java public class Sample { public boolean isEven(List
values) { return values.stream() * .allMatch(v -> v % 2 == 0); } } ``` --- # 確認手順 1. `javac` でコンパイル 2. `javap` でリバースアセンブルする (詳細を出力するため、`-verbose` をつける) ``` javac Sample.java javap -verbose Sample.class ``` → いろいろなことがわかる --- # javap -verbose でわかること **クラスファイルの中身が分かる** * クラス定義・定数プール・フィールド定義 * メソッド * 定義 * **バイトコード** * 属性 * **invokeDynamic で呼び出す Bootstrap メソッド** * レコードクラスの定義 etc... --- # javap の結果 (isEven メソッド) ```java public boolean isEven(List
); 0: aload_1 1: invokeinterface #7, 1 // InterfaceMethod List.stream:()L/Stream; * 6: invokedynamic #13, 0 * // InvokeDynamic #0:test:()L/Predicate; 11: invokeinterface #17, 2 // InterfaceMethod Stream.allMatch:(L/Predicate;)Z 16: ireturn ``` invokeDynamic が使われている! → Bootstrap メソッドは? --- # javap の結果 (Bootstrap) ```java *BootstrapMethods: 0: #49 REF_invokeStatic * java/lang/invoke/LambdaMetafactory.metafactory: ( L/MethodHandles$Lookup; L/String; L/MethodType; L/MethodType; L/MethodHandle; L/MethodType; )L/CallSite; Method arguments: #43 (L/Object;)Z #45 REF_invokeStatic Lambda.lambda$isEven$0:(L/Integer;)Z #48 (L/Integer;)Z ``` --- # Bootstrap の処理 ```java public final class LambdaMetafactory { public static CallSite metafactory(MethodHandles.Lookup caller,省略) throws LambdaConversionException { AbstractValidatingLambdaMetafactory mf; * mf = new InnerClassLambdaMetafactory(Objects.requireNonNull(caller),省略); mf.validateMetafactoryArgs(); * return mf.buildCallSite(); } ``` **インナークラスを生成する**ファクトリークラスが、 **CallSite を生成**している --- # InnerClassLambdaMetafactory **ASM**(Java バイトコード操作フレームワーク)を使い、 **直接バイトコードを組み立てて**クラスを作っている ```java MethodVisitor ctor = cw.visitMethod(ACC_PRIVATE, NAME_CTOR, constructorType.toMethodDescriptorString(), null, null); // メソッド定義 ctor.visitCode(); // コードブロック開始 ctor.visitVarInsn(ALOAD, 0); // 値のロード ctor.visitMethodInsn( INVOKESPECIAL, JAVA_LANG_OBJECT, NAME_CTOR, METHOD_DESCRIPTOR_VOID, false); // メソッド呼び出し ``` --- # buildCallSite() の処理 生成したクラスのインスタンス生成メソッドを CallSite として返す ```java if (factoryType.parameterCount() == 0) { * // In the case of a non-capturing lambda, we * // optimize linkage by pre-computing a single instance Object inst = mh.asType(methodType(Object.class)) .invokeExact(); return new ConstantCallSite( MethodHandles.constant(interfaceClass, inst)); } else { return new ConstantCallSite(mh.asType(factoryType)); } ``` --- # ここまでのまとめ 1. ラムダ式の処理は invokeDynamic を使っている 2. invokeDynamic の Bootstrap メソッドでは、 直接バイトコードを組み立てて**クラスを生成している** 3. 生成したクラスのインスタンス生成メソッドを、 CallSite としている --- class: center, middle ## クラスの中身、気になります! --- # Bootstrap の成果物 * ラムダの Bootstrap メソッドで使用している
`InnerClassLambdaMetafactory` クラスには
**生成したクラスの出力機能**がある --- # 生成したクラスの出力機能 * システムプロパティを指定して実行する
-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles
* DUMP_LAMBDA_PROXY_CLASS_FILES という フォルダにクラスファイルができる
これで Bootstrap の成果物であるクラスがわかる!
--- # 成果物(Sample$$Lambda クラス) クラスファイルを逆コンパイルした結果 ```java final class Sample$$Lambda implements Predicate { public boolean test(Object var1) { return Sample.lambda$isEven$0((Integer)var1); } } ```
lambda$isEven$0
を呼び出している → なにこれ? --- # 謎のメソッドの正体 改めて、検証用の Sample.class を javap してみる (プライベートメソッドも出力するため、`-private` を付ける) ```console javap -private Sample ``` -- ### 結果 ```java public class Sample { public static boolean isEven(List
); * private static boolean lambda$isEven$0(Integer); } ``` --- # lambda$isEven$0 メソッドの正体 メソッドを逆コンパイルした結果 ```java private static boolean lambda$isEven$0(Integer v) { return v % 2 == 0 } ``` → ラムダ式の中身が private メソッドになっている --- # 最終的な処理の流れ
--- exclude: true # ほかのパターン ラムダ式の内容によって、生成されるメソッドやクラスの構造が変わる * ラムダ内で**フィールド変数**を参照している * `lambda$isOdd$0` が**インスタンスメソッド**になる * 今回は、参照していないので static メソッドだった * ラムダ内で**ローカル変数**を参照している * コンストラクタで変数をキャプチャするクラスが生成される * ラムダを実行するごとに、クラスを毎回 `new` する ラムダを書いたときに、 javap したり `-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles` をつけて処理を確認してみよう! --- class: center, middle # 文字列結合と invokeDynamic --- # Java での文字列結合 Java では + 演算子で文字列結合ができる この結合処理の実装が、バージョンアップで変わった ([JEP 280: Indify String Concatenation](https://openjdk.org/jeps/280)) --- # 結合処理の実装 * Java 8 まで * **コンパイル**時に**コンパイラ**が StringBuilder を使って文字列結合する処理を作る * Java 9 以降 * **実行時**に invokeDynamic の **Bootstrap メソッド**が 独自に文字列結合する処理を作る → 確認してみましょう --- # 確認方法 適当な + 演算子で文字列結合するコードを用意 ```java private static String makeText(int count) { * return "Total:" + count + "files"; } ``` Java 8 と Java 9 でコンパイル → javap してみる --- # Java 8 の javap 結果 ```console 0: new #5 // class StringBuilder 3: dup 4: invokespecial #6 // Method StringBuilder."
":()V 7: ldc #7 // String Total: 9: invokevirtual #8 // Method StringBuilder.append:(LString;)LStringBuilder; 12: iload_0 13: invokevirtual #9 // Method StringBuilder.append:(I)LStringBuilder; 16: ldc #10 // String files 18: invokevirtual #8 // Method StringBuilder.append:(LString;)LStringBuilder; 21: invokevirtual #11 // Method StringBuilder.toString:()LString; 24: areturn ``` --- # Java 8 の javap 結果 (概要) * `StringBuilder` に、ひとつづつ `.append` していく * 最後に `.toString()` で `String` を生成 ```java new StringBuilder() .append("Total:") .append(count) .append("files") .toString() ``` --- # Java 9 の javap 結果 ```console * 1: invokedynamic #5, 0 * // InvokeDynamic #0:makeConcatWithConstants: * (I)Ljava/lang/String; ------- BootstrapMethods: 0: #25 REF_invokeStatic java/lang/invoke/StringConcatFactory .makeConcatWithConstants: ( LMethodHandles$Lookup;LString;LMethodType;LString;[LObject; )LCallSite; Method arguments: #26 Total:\u0001files ``` --- # Java 9 の javap 結果 ```console 1: invokedynamic #5, 0 // InvokeDynamic #0:makeConcatWithConstants: (I)Ljava/lang/String; ------- BootstrapMethods: 0: #25 REF_invokeStatic * java/lang/invoke/StringConcatFactory * .makeConcatWithConstants: ( LMethodHandles$Lookup;LString;LMethodType;LString;[LObject; )LCallSite; Method arguments: * #26 Total:\u0001files ``` --- # Java 9 の javap 結果 (概要) * invokeDynamic の Bootstrap で、メソッドを作る * Bootstrap には、テンプレート文字列を渡す * 出来上がったメソッドを実行する --- class: center, middle # Java 8 と Java 9 で # 実行した時の動きを追ってみましょう --- # Java 8 の実行時の動き ```java * new StringBuilder() ``` * StringBuilder を new する → 内部でバッファ(配列)を作る .dotted[ ```java byte[] { □□□□□□□□□□□□□□□□ } ``` ] --- # Java 8 の実行時の動き ```java * .append("Total:") ``` * 文字列を引数に `.append()` メソッドを呼ぶ → 文字列をバッファ(配列)に格納 .dotted[ ```java "Total:" ↓↓↓↓↓↓ byte[] { □□□□□□□□□□ } ``` ] --- # Java 8 の実行時の動き ```java * .append(10) ``` * 数値を引数に `.append()` メソッドを呼ぶ → 数値を文字列に変換して、バッファ(配列)に格納 .dotted[ ```java 10 → "10" ↓↓ byte[] { Total:□□□□ } ``` ] --- # Java 8 の実行時の動き ```java * .append("files") ``` * 文字列を引数に `.append()` メソッドを呼ぶ → バッファ(配列)に …、**空きが足りない** .dotted[ ```java "files" ↓↓↓↓↓ byte[] { Total:10□□ } ``` ] --- # Java 8 の実行時の動き → 新しいバッファを作り、内容をコピー .dotted[ ```java byte[] { Total:10□□ } ↓↓↓↓↓↓↓↓↓↓ byte[] { □□□□□□□□□□□□□□□□□□□□ } ``` ] --- # Java 8 の実行時の動き → バッファ(配列)に文字列を格納 .dotted[ ```java "files" ↓↓↓↓↓ byte[] { Total:10□□□□□□□□□□□□ } ``` ] --- # Java 8 の実行時の動き ```java * .toString() ``` * `.toString()` メソッドを呼ぶ → バッファから String にコピー .dotted[ ```java byte[] { Total:10files□□□□□□□ } ↓↓↓↓↓↓↓↓↓↓↓↓↓ new String(□□□□□□□□□□□□□) ``` ] --- # Java 8 の実行時の動きまとめ * バッファ(配列)に文字列を足していく * バッファが足りなくなったら作り直す * バッファをコピーして String を作る --- # Java 9 以降の場合 invokeDymamic を実行する → 最初に Bootstrap メソッドを呼び出す ```console * 2: invokedynamic #38, 0 * // InvokeDynamic * // #0:makeConcatWithConstants:(I)L/String; ``` --- # Java 9 以降の場合 (Bootstrap) 「Total:
\u0001
files」を 引数に StringConcatFactory.makeConcatWithConstants を呼ぶ ```console BootstrapMethods: 0: #25 REF_invokeStatic * java/lang/invoke/StringConcatFactory * .makeConcatWithConstants: ( LMethodHandles$Lookup;LString;LMethodType;LString;[LObject; )LCallSite; Method arguments: * #26 Total:\u0001files ``` --- # Java 9 以降の場合 (in Bootstrap) Boostrap は、文字列を結合するメソッドを作って返す ```java public static CallSite makeConcatWithConstants( MethodHandles.Lookup lookup, String name, MethodType concatType, String recipe, Object... constants ) throws StringConcatException { (中略) * return new ConstantCallSite( * generateMHInlineCopy(concatType, constantStrings) .viewAsType(concatType, true)); (中略) } ``` --- # Java 9 以降の場合 (in Bootstrap) Boostrap は、文字列を結合するメソッドを作って返す ### 生成するメソッドの内容 * 文字列全体の長さを計算 * バッファを作成 * 文字列を当てはめる * String にする --- # Java 9 以降の場合 (after Bootstrap) invokeDymamic を実行する → 最初に Bootstrap メソッドを呼び出す → 以降、
生成されたクラスのメソッド
を呼び出す --
↑
気になる
--- # 生成されたメソッドの詳細 * 文字列結合の Bootstrap メソッドには 生成したメソッドを出力する機能がない → どうするか -- → **メソッド生成処理を読み解けばいい!** --- ```java String LambdaForm$MH/0x00007a15ef006000(int var0) { // 長さの計算 long indexCoder = 11 + StringConcatHelper.LATIN1; indexCoder += StringConcatHelper.mix(indexCoder, var0); // バッファ(配列)を作って文字列を格納 byte[] buf = StringConcatHelper.newArray("files", indexCoder); indexCoder = StringConcatHelper.prepend(indexCoder, buf, var1, "Total:"); // String にする return new String(buf); } ``` --- # 生成されたメソッドのシグネチャ ```java static String LambdaForm$MH/0x00007a15ef006000(int var0) ``` 定数以外の部分を引数で受けとり、文字列を返すメソッド → 今回は int 型の引数を1つ取る --- # 生成されたメソッドの内容 ```java // 長さの計算 long indexCoder = 11 + StringConcatHelper.LATIN1; indexCoder += StringConcatHelper.mix(indexCoder, var0); ``` * `indexCoder` は
文字列の長さ
+
種類
* 定数 11 (= "Total:" と "files" の文字数の合計) * \+ 種類 LATIN1 (= 0x0) / UTF16 (= 0x1_0000_0000) * \+ 引数0 (数値) の長さ --- ```java byte[] buf = * StringConcatHelper.newArray("files", indexCoder); ``` `newArray` でやっていること → 指定された長さのバッファ(配列)を作る → 文字列をバッファに格納 → 現在の位置を返す .dotted[ ```java "files" ↓↓↓↓↓ byte[] { ・・・・・・・・・・・・・ } ``` ] --- ```java indexCoder = * StringConcatHelper.prepend( * indexCoder, buf, var0, "Total:"); ``` `prepend` でやっていること → 数値と文字列をバッファの指定された位置に格納 → 現在の位置を返す .dotted[ ```java "Total:10" ↓↓↓↓↓↓↓↓ byte[] { ・・・・・・・・files } ``` ] --- ```java *return new String(buf); ``` String を作る バッファはコピーせずにそのまま使う .dotted[ ```java byte[] { Total:10files } ↓ String(value, coder); ``` ] --- # 実行結果の比較 * Java 8 まで * StringBuilder を使った処理 * ちょっとずつバッファに文字列を足していく * バッファの作り直しが発生する * Java 9 以降 * invokeDynamic を使った処理 * 必要な長さを計算して、バッファを作る * バッファの作り直しが発生しない 実行時の処理は、**Java 9 以降の方は無駄がない** --- # invokeDynamic の可能性 * invokeDynamic を使えば実行時に処理を差し込める * コンパイル時にやるようなことを実行時に行える * 最適な処理を生成しやすい さらに、まだ Java で使っていない機能もある * CallSite の MethodHandle の更新 * Java でも動的ディスパッチしたい場合がでてきたら使うかも!? --- # まとめ * もともと invokeDynamic は JVM で動的型付け言語を 効率的に処理するために実装された * 現在は、Java でも使われている * Lambda の実装 * 文字列結合の処理生成 など --- # まとめ * もともと invokeDynamic は JVM で動的型付け言語を 効率的に処理するために実装された * 現在は、Java でも使われている * Lambda の実装 * 文字列結合の処理生成 など * 今後はさらに使われるようになるかも! --- class: center, middle # InvokeDynamic, # Under the Hood YujiSoftware Yuichi Sakuraba