HotSpotの反撃:JEP 544はGraalVMの起動時間独占を終わらせるのか?
HotSpot JVMにとってエキサイティングな時代が来ました。JEP 544: Ahead-of-Time Code CompilationがJava 28向けに提案されたばかりです。これにより、GraalVMの厄介な制約を一切伴わずに、HotSpotのAOTキャッシュがGraalVMに匹敵する起動パフォーマンスを得られることが約束されます。ただし、AOTキャッシュのトレーニングはGraalVMによる事前コンパイルとは異なるものです。では、両者は実際のところどのように比較できるのでしょうか?AOTキャッシュの背景
Javaは20年以上にわたって起動速度の改善に取り組んできました。Java 5でのクラスデータ共有(CDS)に始まり、Java 10ではAppCDSによりアプリケーションクラスが対象に加わり、Java 13ではトレーニングプロセスの効率化が図られました。しかし、いずれも数値の面でも採用実績の面でも、本当に印象的なインパクトを与えたとは言えません。
2022年、Mark ReinholdはJavaの起動を大幅に改善することを目指す「Project Leyden」を発表しました。時は流れてJava 24では、HotSpotのAOTキャッシュであるJEP 483が組み込まれました。AOTキャッシュはCDSのアイデアを利用しつつ、トレーニング実行の後にクラスのリンク済み状態やアーカイブされたヒープオブジェクトも保存する、より汎用的なメカニズムを追加しています。
Java 25ではJEP 514によりAOTキャッシュが実用化され、JEP 515によりメソッドプロファイリング結果のキャッシュが追加されました。そして、コンパイル済みメソッドをキャッシュする提案中のJEP 544によって、Project LeydenはJavaの起動に対して約束された画期的な改善を届ける最終段階に入っています。
どれくらい速くなるのか?
ほとんどのパフォーマンス改善と同様に、実際の数値はアプリケーションが何をしているかによって大きく変わります。現実世界での好例がinstall4jのIDEです。メインウィンドウを表示する前に、起動時に1万個のクラスをロードする必要があります。筆者のMacBookでは、これまでこれに3秒ほどかかっていました。Java 25のAOTキャッシュを使用すると、1.5秒で立ち上がるようになりました。2倍の高速化は驚異的な達成であり、ユーザーエクスペリエンスにとって非常に重要です。体感は「重い」から「キビキビ」へと一変します。
Java 28では、JDKをアップグレードするだけで、さらに同様の改善がもたらされる見込みです。複雑なIDEの起動時間が1秒未満というのは信じられないほど速く、「Javaの起動は遅い」という古い先入観は永久に捨て去らなければならないでしょう。以下は、このJEPに示されているパフォーマンスグラフです:
これは実際のところ驚くべきもので、以前の状況がどれほど悪かったかを物語っています。
GraalVMはどうなのか?
ポリグロットVMおよびC2 HotSpotコンパイラの代替としてのルーツから、GraalVMはJVMバイトコード向けのネイティブイメージジェネレータへと姿を変えました。Quarkus、Helidon、Spring Bootをはじめ、エコシステムでは幅広く採用されています。
GraalVMを使えば、JVMを必要としない単一のバイナリが得られ、起動時にもワークロード実行時にも優れたパフォーマンスが得られます。
それほど素晴らしいなら、なぜ誰もが使わないのでしょうか?多くの優れたものと同様に、落とし穴があります。それはクローズドワールドの要件です。プログラムが実行できるすべてのことは、ビルド時に既知である必要があります。リフレクション、動的プロキシ、リソース、シリアライゼーションなど、動的な振る舞いは事前に宣言しなければなりません。さらに、SwingアプリケーションはGraalVMネイティブイメージではまったくサポートされていません(install4jのIDEもその一つです)。
両者を比較してみる
比較にあたっては、GraalVMに有利なアプリケーションをあえて選びました。BetterBranchは、現在のブランチに対するすべてのローカルGitブランチのahead/behind状態を表示する便利なコマンドラインツールで、GraalVMバイナリとしてデプロイされています。
以下は、Linux上でhyperfineを使用して測定した数値です。「実ワークロード」列はBetterBranchを独自のリポジトリに対して実行した結果で、「即時終了」はGitリポジトリが存在しないディレクトリでツールを実行し、エラーメッセージとともに直ちに終了するものです。
| 方式 | 即時終了 | 実ワークロード |
|---|---|---|
| プレーンなJVM | 29.8 ms | 280.5 ms |
| AOTコードなしのAOTキャッシュ | 25.2 ms | 195.1 ms |
| AOTコードありのAOTキャッシュ(JEP 544) | 26.1 ms | 131.6 ms |
| GraalVMネイティブイメージ | 3.4 ms | 50.1 ms |
JEP 544はまだJDK 28の早期アクセスビルドには含まれていないため、AOTコードの数値はProject Leydenの早期アクセスビルドで測定しました。
実ワークロード列の測定では、JVMを2コアに固定したため、JITコンパイラがアプリケーションとCPUを取り合います。これはJEP 544のパフォーマンスグラフが測定された環境であり、キャッシュされたAOTコードが真の効果を発揮するケースです。AOTコードなしのキャッシュと比べて実経過時間(wall time)の33%を節約でき、プレーンなJVMと比べると53%の節約になります。


では、その差はわずかなのか?いいえ、GraalVMはまだ大きくリードしています。しかし、その差は着実に縮まっています。コンテナ化されたアプリケーションの起動でミリ秒が重要なら、GraalVMを選びましょう。アプリケーションがクローズドワールドの制約に対応できないなら、AOTを使いましょう。AOTはJava 25で利用可能になっており、Java 28ではさらに素晴らしいものになります。
トレーニングはどう行う?
AOTキャッシュにはまだ疑問が残ります。実際の運用では、どのようにキャッシュを作成するのでしょうか?アプリケーションを社内でデプロイするなら、ビルドシステムやデプロイスクリプトの中でトレーニング実行を行えます。しかし、アプリケーションを出荷する場合には、どのような選択肢があるのでしょうか?トレーニングは、アプリケーションが実際に動作するシステム上で行わなければなりません。
デスクトップアプリケーションの場合、顧客のマシン上でトレーニング実行を仕込むのはかなり非現実的に思えます。トレーニング実行のために特別なVMパラメータを追加し、その後の実行で記録済みのAOTキャッシュを使うために別のパラメータを追加する必要があります。AOTキャッシュによる高速化はデスクトップアプリケーションのUXにとって非常に重要ですが、ユーザーにそのような手順への協力を強いるのは魅力的ではありません。
そこで登場するのがinstall4jのAOTキャッシュ機能です。install4jのランチャーは、VMパラメータとして-XX:AOTCacheOutput=<cache file>を渡すことで、アプリケーションの初回起動時にAOTキャッシュを記録できます(オプション)。その後の実行では、-XX:AOTCache=<cache file>によりキャッシュが使用されます。
デフォルトでは、キャッシュはユーザーごとのキャッシュディレクトリに保存されます。キャッシュファイル名には、アプリケーションのビルドとJavaランタイムから導出されたキーが含まれるため、アプリケーションの更新後やJavaランタイムの変更後にはキャッシュが自動的に再生成されます。以前のビルドのキャッシュは自動的にクリーンアップされ、アンインストーラーが作成されたすべてのキャッシュを削除します。
トレーニング実行をプログラム的に終了させたい場合には、Java 25.0.4以降で利用可能なHotSpotAOTCacheMXBean MBeanを使用できます。このMBeanではendRecording()を呼び出せます。たとえば、ユーザーインターフェースの表示後、初期のバックグラウンドタスクが完了した時点で呼び出すといった具合です。


コマンドラインアプリケーションの場合、即時終了パスを伴うトレーニング実行をインストーラーの中で仕込むのが最善の選択肢です。これにより、現状避けられないトレーニングのターミナル出力をユーザーに見せずに済みます。install4jのコマンドラインコンパイラでは、--versionフラグを付けて実行することでこれを実現しています。
結論
JVMにとって最悪のシナリオを示すために、GraalVMに有利な数値になる例をあえて選びました。そうなるのは、アプリケーションが非常に小さいからです。より大きなアプリケーションでは、JVMを立ち上げる必要がないというGraalVMの利点は重要度を失っていきます。アプリケーションの実際の処理部分が大きくなるにつれて、この固定の優位性は比例して縮み、AOTコードキャッシュが追い上げてきます。どれだけ追い上げるかは、測定するアプリケーションに完全に依存します。弊社のinstall4j IDEでは、AOTコードなしのAOTキャッシュだけで起動時間が50%短縮されました。上記のより小さいコマンドラインアプリケーションでは約30%です。
明らかなのは、AOTキャッシュを使ったJVMの起動時間がGraalVMに対して競争力を増してきているということであり、GraalVMが要求する制約を考えればなおさらです。Java 25の現時点でも十分に優れており、Java 28ではこの点がさらに改善されます。
ソフトウェアを出荷していて、install4jのAOTキャッシュ記録機能を活用して起動時間を短縮したい場合は、登録なしでぜひお試しください。