クラス初期化の明示的な指定

デフォルトでは、ネイティブ・イメージは実行時にアプリケーション・クラスを初期化します。ただし、ネイティブ・イメージがビルド時に初期化が安全であると証明しているクラスは除きます。ただし、ビルド時または実行時に初期化するクラスを明示的に指定することで、デフォルトの動作に影響を与えることができます。そのため、--initialize-at-build-timeおよび--initialize-at-run-timeという2つのコマンドライン・オプションがあります。これらのオプションを使用して、パッケージ全体または個々のクラスを指定できます。たとえば、クラスp.C1p.C2、...、p.Cnがある場合、次のオプションをnative-imageに渡すことによって、パッケージp内のすべてのクラスがビルド時に初期化されるように指定できます:

--initialize-at-build-time=p

パッケージpのクラスC1のみを実行時に初期化する場合は、次を使用します:

--initialize-at-run-time=p.C1

ネイティブ・イメージ機能インタフェースRuntimeClassInitializationクラスを使用して、プログラムでクラスの初期化を指定することもできます。

このガイドでは、実行時(デフォルトの動作)およびビルド時にクラス・イニシャライザを実行してネイティブ実行可能ファイルをビルドする方法を示し、その2つのアプローチを比較します。

前提条件

GraalVM JDKがインストール済であることを確認します。最も簡単に始めるには、SDKMAN!を使用します。その他のインストール・オプションについては、ダウンロード・セクションにアクセスしてください。

デモの実行

このデモでは、2023年からのJavaのトークを解析する単純なJavaアプリケーションを実行します。パーサーはレコードを作成し、それらをList<Talk>コレクションに追加します。

  1. 次のJavaソース・コードをTalkParser.javaという名前のファイルに保存します:
     import java.util.ArrayList;
     import java.util.List;
     import java.util.Scanner;
    
     public class TalkParser {
       private static final List<Talk> TALKS = new ArrayList<>();
       static {
         Scanner s = new Scanner("""
             Asynchronous Programming in Java: Options to Choose from by Venkat Subramaniam
             Anatomy of a Spring Boot App with Clean Architecture by Steve Pember
             Java in the Cloud with GraalVM by Alina Yurenko
             Bootiful Spring Boot 3 by Josh Long
             """);
         while (s.hasNextLine()) {
           TALKS.add(new Talk(s.nextLine()));
         }
         s.close();
       }
    
       public static void main(String[] args) {
         System.out.println("Talks loaded using scanner:");
         for (Talk talk : TALKS) {
             System.out.println("- " + talk.name());
         }
       }
     }
    
     record Talk (String name) {}
    
  2. アプリケーションをコンパイルします:
     javac TalkParser.java
    
  3. ネイティブ実行可能ファイルをビルドし、実行時にクラス・イニシャライザを明示的に実行します:
     native-image --initialize-at-run-time=TalkParser,Talk -o runtime-parser TalkParser
    

    この例の--initialize-at-run-time=TalkParser,Talkオプションは省略できます。これらのクラスは、実行時にデフォルトで初期化されるようにマークされているためです。-oオプションでは、出力ファイルの名前を指定します。

  4. timeを使用してネイティブ・アプリケーションを実行します:
     time ./runtime-parser
    

    16GBのメモリーと8コアを持つマシンでは、次のような結果が表示されます:

     Talks loaded using scanner:
     - Asynchronous Programming in Java: Options to Choose from by Venkat Subramaniam
     - Anatomy of a Spring Boot App with Clean Architecture by Steve Pember
     - Java in the Cloud with GraalVM by Alina Yurenko
     - Bootiful Spring Boot 3 by Josh Long
     ./runtime-parser  0.00s user 0.00s system 52% cpu 0.010 total
    

    アプリケーションは実行時にテキスト・ブロックを解析します。

    ファイル・サイズが約13Mであることを確認します:

     du -sh runtime-parser
    
  5. 次に、ビルド時にTalkParserを初期化し、前のビルドと区別するために出力ファイルに別の名前を指定して、ネイティブ実行可能ファイルをビルドします。Talkレコードも明示的に初期化する必要があるため、このタイプのオブジェクトはイメージ・ヒープに保持されます。
    native-image --initialize-at-build-time=TalkParser,Talk -o buildtime-parser TalkParser
    

    アプリケーションがイメージ・ヒープに他のタイプを追加する場合は、ビルド時の初期化のために各タイプ(または対応するパッケージ)を明示的にマークする必要があります。適切なアクション可能エラー・メッセージによって、プロセスが順を追って進められます。

  6. 比較のためにtimeを使用して2番目の実行可能ファイルを実行します:
     time ./buildtime-parser
    

    今回は、次のように表示されます:

     Talks loaded using scanner:
     - Asynchronous Programming in Java: Options to Choose from by Venkat Subramaniam
     - Anatomy of a Spring Boot App with Clean Architecture by Steve Pember
     - Java in the Cloud with GraalVM by Alina Yurenko
     - Bootiful Spring Boot 3 by Josh Long
     ./buildtime-parser  0.00s user 0.00s system 53% cpu 0.016 total
    

    ファイル・サイズが約6.4Mに減っていることを確認します。

     du -sh buildtime-parser
    

    ファイル・サイズの変化の原因は、ネイティブ・イメージがビルド時に静的イニシャライザを実行し、テキスト・ブロックを解析して、実行可能ファイルにTalkレコードのみを保持しているためです。

    その結果、ネイティブ・イメージがアプリケーションを静的に分析する場合、スキャン・インフラストラクチャの大部分は到達可能にならないため、実行可能ファイルに含まれません。

アプリケーションをより正確にプロファイリングするためのもう1つの重要な基準は、Linux perfプロファイラを使用して取得できる命令の数です。

たとえば、このデモ・アプリケーションでは、ビルド時のクラス初期化の場合、命令の数はほぼ30%減少しました(11.8Mから8.6Mまで):

perf stat ./runtime-parser 
Talks loaded using scanner:
- Asynchronous Programming in Java: Options to Choose from by Venkat Subramaniam
(...)
 Performance counter stats for './runtime-parser':
(...)                   
        11,323,415      cycles                           #    3.252 GHz                       
        11,781,338      instructions                     #    1.04  insn per cycle            
         2,264,670      branches                         #  650.307 M/sec                     
            28,583      branch-misses                    #    1.26% of all branches           
(...)   
       0.003817438 seconds time elapsed
       0.000000000 seconds user
       0.003878000 seconds sys 
perf stat ./buildtime-parser 
Talks loaded using scanner:
- Asynchronous Programming in Java: Options to Choose from by Venkat Subramaniam
(...)
 Performance counter stats for './buildtime-parser':
(...)                    
         9,534,318      cycles                           #    3.870 GHz                       
         8,609,249      instructions                     #    0.90  insn per cycle            
         1,640,540      branches                         #  665.818 M/sec                     
            23,490      branch-misses                    #    1.43% of all branches           
(...)
       0.003119519 seconds time elapsed
       0.001113000 seconds user
       0.002226000 seconds sys 

これは、ネイティブ・イメージが作業を実行時からビルド時に移行する方法を示しています。ビルド時にクラスが初期化される場合、実行可能ファイルがビルドされるときにテキスト・ブロックが解析され、解析されたオブジェクトのみが含まれます。これにより、実行可能ファイルのサイズが小さくなるだけでなく、実行速度も速くなります。実行可能ファイルが実行されるときに、Talkレコードがすでに存在するため、それを出力するだけで済みます。

ネイティブ・イメージを使用してビルドされたネイティブ実行可能ファイルがHotSpotの動作と可能なかぎり互換性を持つように、ビルド時に安全に初期化できないアプリケーション・クラスは、実行時に初期化されます。ユーザーまたは使用するフレームワークは、ファイル・サイズが小さくなり、実行時間が短くなるという利点を得るため、特定のクラスのビルド時の初期化を明示的にリクエストする必要があります。適切なデータ構造を含めて、イメージ・サイズが増加しないようにします。また、単一のクラスのみで--initialize-at-build-timeを使用することもお薦めします。場合によっては、多数の--initialize-at-build-timeエントリを追加する必要があります。ビルド時の初期化が正しくないと、異常な動作が生じたり、パスワードや暗号化キーといった機密データが含まれたりするなど、本番設定で回避する必要のある問題が発生する可能性があります。

まとめ

このガイドでは、デフォルトのnative-imageクラスの初期化ポリシーに影響を与え、ユース・ケースに応じてビルド時に特定のクラスを初期化するように構成する方法を示しました。ビルド時と実行時の初期化の利点は、「ネイティブ・イメージのクラスの初期化」で説明されていますが、要するに、ビルド時の初期化によってファイル・サイズ全体が大幅に減少し、正しく使用されるとアプリケーションの実行時間が改善される可能性があります。