invokedynamic
#Java #JVM
参考
Java Magazine
https://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-SO17-MethodInvoc.pdf
https://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-ND17-MethodInvoc2.pdf
Baeldung
https://www.baeldung.com/java-invoke-dynamic
https://www.baeldung.com/java-string-concatenation-invoke-dynamic
主に上記の記事を読んで理解したことをまとめてる
invokedynamicとは
メソッド呼び出しに使われるJVMのバイトコード(命令コード)の一つ
Java7から実装された
LambdaやRecord、Java9以降のStringの結合などに使われている
JSR292で定義されている
https://www.baeldung.com/java-invoke-dynamic
他のメソッド呼び出しとの違い
他のメソッド呼び出しの込み入った説明については以下の資料参照
https://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-SO17-MethodInvoc.pdf
invokevirtual
仮想ルックアップ(extendsしたスーパークラス内に定義されているメソッドを探す)してメソッドを呼び出す
invokeinterface
仮想ルックアップ(implementしたスーパークラス/インターフェース内に定義されているメソッドを探す)してメソッドを呼び出す
継承階層が異なる関係でinvokevirtualよりも複雑なルックアップ作業になる
invokestatic
静的にstaticメソッドを呼び出す
invokespecial
オーバーライドの可能性がないメソッド(private, constructorなど)を仮想ルックアップせずに呼び出す
それぞれ違いはあれど上記の4つは、呼び出す先が決まっている(invokevirtualとかも継承構造のテーブルを参照して実際呼び出すメソッドが動的に決まるとはいえ)という点で共通してる。動的にリンクする方法がないので、あとから動的にこれらの動作をカスタムできないという問題がある (これがなぜ問題なのかあまりピンと来なかったけど、やはり言語のバージョンが上がって、例えばより効率的な機能が実装されたときとかなのかな)。
上記の制約の代表的な回避策として、コンパイル時と実行時に対策が入れられることがある
動的なアプローチはリフレクションベースの対策になるので非効率的である
これはわかる
コンパイル時のアプローチは実行時は効率的だが、もろくなりやすくバイトコードが増えて起動時間が遅くなりがちである
コンパイル時に問題が見つかるほう早くていいじゃんと思うことは多くて、これは最初ピンとこなかった
後述しているが、コンパイル時のアプローチはというのは基本的にclassファイル内のバイトコードに手が入るということ。
もろくなりやすいというはなしは、BootstrapとlinkageロジックをJavaで書くのはbytecodeを生成するためのAST書くより楽なので壊れにくいというBaeldung記事筆者の評価であると思う
invokedynamicは前述したメソッド呼び出し処理よりもより動的に呼び出しを行う
Bootstrap Method (BSM)をinvokestaticで呼び出して、CallSiteを取得する
https://docs.oracle.com/javase/jp/8/docs/api/java/lang/invoke/CallSite.html
CallSiteは以下の情報を持つ
実際に呼び出すメソッドのMethodHandle
https://docs.oracle.com/javase/jp/8/docs/api/java/lang/invoke/MethodHandle.html
ReflectionAPIとは別のメソッド、フィールドなどへの直接実行可能な型付の参照
一度呼び出してしまえば、CallSite & MethodHandleが取得できるのでその後はBootstrap Methodは挟まずに実際に呼び出すメソッドを呼び出せる
呼び出し先メソッドを生成するためのファクトリのようなものと考えると良さそう
Lambdaの場合はLambdaMetafactory, String Concatenationの場合はStringConcatFactoryだったり実態が変わってくる
https://docs.oracle.com/javase/jp/8/docs/api/java/lang/invoke/LambdaMetafactory.html
https://docs.oracle.com/javase/jp/9/docs/api/java/lang/invoke/StringConcatFactory.html
makeConcatのパラメータの説明を読むと、 invokedynamicで使用されるときは、これはVMによって自動的にスタックされます。 との説明があってVMが呼び出し先を決めていることがわかる
CallSite経由で取得したMethodHandle経由で、実際の処理を実行
例えばLambda生成時も匿名の内部クラスをコンパイル時に生成するような静的なアプローチでもなく、リフレクションAPIにも頼っていないアプローチなんですよーとうことだと思う
MethodHandleは最初にオブジェクト作成した時のみ諸々のチェックを実施するのでReflectionAPIによる呼び出しよりもパフォーマンスがよいですよという話もある
https://dev.java/learn/introduction_to_method_handles/#conclusion
invokedynamicの利点
シンプルなコンパイル時のアプローチに比べた利点
Lamda実行時の話で行くと匿名クラスができたりしないのでfootprintが少なくなる
メソッドへのlinkage(メソッドの呼び出し)がバイトコードだよりから、bootstrap methodに委譲されるのでバイトコードが小さくなる (起動速度が向上しやすくなる)
このあたりはJavaのコンパイラやJVMの開発者にとっても、処理の改善が入れやすくなぅて開発効率が上がるみたいな利点もありそう
例えば実行環境のJavaのバージョンが上がって、bootstrapメソッドが効率的になったとして、アプリ側を再コンパイルせずにこの改良を利用できる
バイトコード側にベタッとこれが書いてあるとアプリ側を再コンパイルする必要があるよと
JavaMagazineの記事から抜粋
現在の実装では、ラムダ式ごとにインナー・クラスを作成するprivateなメタファクトリを引き続き使用していますが、クラスは動的に作成され、ディスクに書き込まれることはありません。つまり、今後のJavaリリースで実装メカニズムが変わる可能性があり、そうなれば、既存のラムダ式は新しいメカニズムのメリットを受けられるようになります
この実装は、ラムダ式の実装型を表す動的クラスを作成すると同時に、実装の将来性を保証し、JITとの親和性も維持しています。
例えば
単純な文字列結合のコードがJava8で動いていた場合、バイトコード上はStringBuilderが生成されて文字列が結合される
これをJava9の実行環境で動かしても、バイトコード上StringBuilder使って結合されてるわけだから、String Concatenationは使われない
一度Java9以降でコンパイルし直せば、バイトコード上、当該の文字列結合はString Concatenation(invoke dynamic)が使われるようになる
ここまでくれば、例えば将来的なJavaのバージョンで文字列結合の改善が入ったとしても(再コンパイルせずに)そのままその改善の恩恵を受けられる (invoke dynamicで呼び出した先が最適化される形になるわけなので)
この辺への振り方がJavaの思想を表している気がしていて、やっぱり後方互換というのは本当に強く意識されているんだなと思う
https://dev.java/evolution/#thoughtful-evolution