JDK 24 で追加された JEP 483: Ahead-of-Time Class Loading & Linking

blog1.mammb.com


はじめに

JDK24 にて、クラス・データ共有(CDS)が拡張され、旧来からあるアプリケーションクラスの事前キャッシュに加え、リンクされたフォームを扱えるようになりました。 キャッシュ保存することで、アプリケーションの起動時間とウォームアップ時間を短縮できます。

  • ほとんどのアプリケーションは、実行するたびにほぼ同じ方法で起動するという事実を利用して、起動時間を改善する
  • アプリケーション、ライブラリ、フレームワークのコードを変更する必要はない
  • この機能に直接関係するコマンドラインオプションを除いて、javaランチャーを使用してコマンドラインからアプリケーションを起動する方法に変更を加える必要はない
  • jlink または jpackage ツールの使用を必要としない
  • HotSpot JVMがアプリケーションのコードを最適化して最高のパフォーマンスを実現するのに必要な時間を継続的に改善するための基盤を提供

現時点では、アプリケーションに代表的なワークロードを実行させAOTキャッシュを生成する といった追加の手順が必要になります(将来的にはこの手順が簡略化される予定です)。


Ahead-of-Time Class Loading & Linking に至る歴史

JDK では旧来からクラス・データ共有(CDS)が段階的に拡張されてきました。簡単に流れを見ておきましょう。

  • JDK 1.5 でクラス・データ共有(CDS)が導入された
    • インストーラがシステムJARファイルから一連のクラスを専用の内部表現にロードして共有アーカイブを作成(手動で作成することも可能)
    • JVM の呼び出し中に、共有アーカイブがメモリマップされ、クラスのロードコストが節約でき、複数の JVM プロセスで共有(現在では、アドレス空間レイアウト・ランダム化(ASLR)のようなセキュリティ機能によりJVM プロセスで共有されることは無くなっている)
  • JDK 10 でアプリケーションクラスを共有アーカイブに配置可能になった(JEP 310: Application Class-Data Sharing)
    • AppCDS は「1回以上のアプリケーションの試行によるクラスリスト作成」「クラスリストを使用してアーカイブをダンプ」「アーカイブを指定してアプリケーションを実行」の手順が必要
  • JDK 12 で CDSアーカイブはJDKバイナリに事前パッケージ済となった(JDKのビルド時に生成)(JEP 341: Default CDS Archives)
    • Linux/macOSプラットフォームの場合、共有アーカイブは/lib/<arch>/server/classes.jsa
    • Windowsプラットフォームの場合、共有アーカイブは /bin/server/classes.jsa
  • JDK13 で AppCDSをアプリケーションの実行終了時にクラスを動的にアーカイブできるようになった(JEP 350: Dynamic CDS Archives)
    • -XX:ArchiveClassesAtExit オプションでアプリケーションを実行するとAppCDSが生成される
  • JDK24 のAOTキャッシュは、クラスを読み込み、解析し、ロードし、リンクした後に保存される


AOTキャッシュの作成

キャッシュを作成するには、2つのステップが必要です。

最初に、アプリケーションをトレーニング実行で一度実行し、AOTコンフィギュレーション(app.aotconf)を記録します。

$ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf \
       -cp app.jar com.example.App ...

次に、AOTコンフィギュレーションからAOTキャッシュを生成します(このステップではアプリケーションは実行されない)。

$ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf \
       -XX:AOTCache=app.aot -cp app.jar

AOTキャッシュが作成できれば、AOTキャッシュを使ってアプリケーションを実行できます。

$ java -XX:AOTCache=app.aot -cp app.jar com.example.App ...

AOTキャッシュにより、JVMが通常ジャスト・イン・タイムで行うはずの読み取り、解析、ロード、リンク作業を先行して行われ、起動時間が短縮されます。

JVMがAOTキャッシュを使用するように正しく設定されているかどうかを確認するには、-XX:AOTMode=on オプションを使います。

$ java -XX:AOTCache=app.aot -XX:AOTMode=on \
       -cp app.jar com.example.App ...

このオプションにより、AOTキャッシュが利用できない場合はエラーとなりアプリケーションは終了します。-XX:AOTMode=on オプションを指定しない場合は、警告を出力してキャッシュを無効化して継続します。

AOTキャッシュのオプションには以下のようなものがあります。

オプション 説明
-XX:AOTMode=record AOTキャッシュ記録モード
-XX:AOTMode=create AOTキャッシュ生成モード
-XX:AOTMode=on AOTキャッシュ必須実行
-XX:AOTMode=off AOTキャッシュを無効化
-XX:AOTConfiguration=xxx.aotconf AOTコンフィギュレーションのパスを指定
-XX:AOTCache=xxx.aot AOTキャッシュのパスを指定
-XX:-AOTClassLinking AOTキャッシュのロードとリンクを無効化(旧来のアプリケーションCDSと同等になる)


JDK25 でコマンドラインオプションが改善されました。以下を参照してください。

blog1.mammb.com


AOTキャッシュの制限

  • トレーニング実行中に生成された AOT キャッシュの利点を享受するためには、それ以降のすべての実行が基本的に類似していなければならない
  • すべての実行は、同じ JDK リリースを使用し、同じハードウェア・アーキテクチャとオペレーティング・システム上でなければならない
  • すべての実行は、一貫したクラス・パスを持っていなければならない(クラス・パスにはJARファイルのみを指定可能でクラス・パス内のディレクトリはサポートされない)
  • すべての実行は、コマンドラインのモジュール・オプションとモジュール・グラフが一致していなければならない
  • m--module--p--module-path--add-modules--enable-native-access オプションの引数は、存在する場合、同一でなければならない
  • add-exports--add-opens--add-reads--illegal-native-access--limit-modules--patch-module--upgrade-module-path オプションは使用できない
  • ClassFileLoadHook を使用してクラスファイルを任意に書き換えることができる JVMTI エージェントは使用できないを使用してはならない
  • すべての実行は、AddToBootstrapClassLoaderSearch および AddToSystemClassLoaderSearch APIを呼び出すJVMTIエージェントを使用してはならない
  • ZGCはまだサポートされていない


トレーニング・ランのヒント

  • AOT キャッシュは、トレーニング・ランが本番運用と同じようなことをする限りにおいてのみ役立つ
  • AOTキャッシュのサイズを最小化するには、トレーニング・ランで本番環境で使用しないクラスをロードしないようにする
  • 実運用環境において、アプリケーションがネットワーク上の他のホストとやりとりしたり、 データベースにアクセスしたりする場合、必要なクラスが確実にロードされるようにするために、 これらのやりとりをモックする必要があるかもしれない(トレーニングの実行に含めることができない場合、ジャストインタイムでロードされる)
  • 実運用環境と同じ環境でトレーニング・ランを実行できない場合は、可能な限り実際の本番稼動に類似した合成トレーニング稼動を作成することを推奨する(トレーニング専用の2つ目のメインクラスをアプリケーションに追加するなど)
  • 起動時間を最適化するには、本番環境での起動時にロードされるクラスと同じクラスをトレーニング実行でもロードするように構成する(どのクラスがロードされるかは、-verbose:class コマンドラインオプションまたは JDK Flight Recorder の jdk.ClassLoad イベントで確認できる)


AOTキャッシュの効果

Stream APIを使用している短いプログラムを例にします。この例では、600近くのJDKクラスが読み込まれ、解析され、ロードされ、リンクされる。

import java.util.*;
import java.util.stream.*;

public class HelloStream {

    public static void main(String ... args) {
        var words = List.of("hello", "fuzzy", "world");
        var greeting = words.stream()
            .filter(w -> !w.contains("z"))
            .collect(Collectors.joining(", "));
        System.out.println(greeting);  // hello, world
    }

}

AOTキャッシュ無しの JDK 23 で 0.031 秒が、AOTキャッシュを利用した場合 0.018 秒で実行されます(AOTキャッシュは11.4メガバイト)。

Spring boot PetClinic(バージョン3.2.0)では、AOTキャッシュ無しの JDK 23 で 4.486 秒で起動が、AOTキャッシュを利用した場合 2.604 秒で起動します(AOTキャッシュは130メガバイト)。

-XX:-AOTClassLinkingでロード・リンクを無効化した場合も含めた、それぞれの起動時間の比較は以下となります。

HelloStream PetClinic
JDK 23(AOTキャッシュ無し) 0.031 4.486
AOTキャッシュ(-XX:-AOTClassLinking) 0.027 (+13%) 3.008 (+33%)
AOTキャッシュ 0.018 (+42%) 2.604 (+42%)

Spring boot PetClinic では、旧来から利用可能なアプリケーションCDS機能で十分な起動時間の短縮になることがわかります。 小さなプログラムでは、ロードとリンクの関与度合が大きくなります。