◀戻る
クラス初期化を明示的に指定する
デフォルトでは、ネイティブイメージは、ネイティブイメージがビルド時に初期化しても「安全」であると証明されたクラスを除き、アプリケーションクラスを実行時に初期化します。ただし、ビルド時または実行時に初期化するクラスを明示的に指定することで、デフォルトの動作を変更できます。そのためには、`--initialize-at-build-time` と `--initialize-at-run-time` の2つのコマンドラインオプションがあります。これらのオプションを使用して、パッケージ全体または個々のクラスを指定できます。たとえば、`p.C1`、`p.C2`、…、`p.Cn` というクラスがある場合、パッケージ `p` 内のすべてのクラスをビルド時に初期化するように指定するには、次のオプションを `native-image` に渡します。
--initialize-at-build-time=p
パッケージ `p` のクラス `C1` のみを実行時に初期化する場合は、以下を使用します。
--initialize-at-run-time=p.C1
ネイティブイメージ機能インターフェースの `RuntimeClassInitialization` クラスを使用して、プログラムでクラスの初期化を指定することもできます。
このガイドでは、クラスイニシャライザを実行時に(デフォルトの動作)、次にビルド時に実行することでネイティブ実行可能ファイルをビルドする方法を示し、2つのアプローチを比較します。
前提条件
GraalVM JDKがインストールされていることを確認してください。最も簡単に始める方法は、SDKMAN!を使用することです。その他のインストールオプションについては、ダウンロードセクションをご覧ください。
デモを実行する
デモとして、2023年の一部のJava講演を解析する単純なJavaアプリケーションを実行します。パーサーはレコードを作成し、それらを `List<Talk>` コレクションに追加します。
- 次の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) {}
- アプリケーションをコンパイルします。
javac TalkParser.java
- クラスイニシャライザを実行時に明示的に実行して、ネイティブ実行可能ファイルをビルドします。
native-image --initialize-at-run-time=TalkParser,Talk -o runtime-parser TalkParser
この例では、これらのクラスはデフォルトで実行時に初期化されるようにマークされているため、`--initialize-at-run-time=TalkParser,Talk` オプションを省略できます。 `-o` オプションは出力ファイルの名前を指定します。
- ネイティブアプリケーションを実行し、`time` で時間を計測します。
time ./runtime-parser
16 GBのメモリと8コアのマシンでは、次のような結果が表示されます。``` Talks loaded using scanner
- Venkat SubramaniamによるJavaの非同期プログラミング:選択可能なオプション
- Steve Pemberによるクリーンアーキテクチャを備えたSpringBootアプリの構造
- Alina YurenkoによるGraalVMを使用したクラウドでのJava
- Josh Longによる魅力的なSpringBoot 3 ./runtime-parser 0.00s user 0.00s system 52% cpu 0.010 total ``` アプリケーションは実行時にテキストブロックを解析します。
ファイルサイズを確認します。約13Mである必要があります。
du -sh runtime-parser
- 次に、`TalkParser` をビルド時に初期化し、以前のビルドと区別するために出力ファイルに別の名前を指定して、ネイティブ実行可能ファイルをビルドします。 `Talk` レコードも明示的に初期化する必要があるため、このタイプのオブジェクトは実行可能ヒープに永続化されます。
native-image --initialize-at-build-time=TalkParser,Talk -o buildtime-parser TalkParser
アプリケーションがイメージヒープに追加のタイプを追加する場合、各タイプ(または対応するパッケージ)はビルド時の初期化のために明示的にマークする必要があります。適切な対応可能なエラーメッセージがプロセスをガイドします。
- 2番目の実行可能ファイルを実行して `time` で時間を計測し、比較します。
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` クラス初期化ポリシーにどのように影響を与え、ユースケースに応じてビルド時に特定のクラスを初期化するように構成できるかを示しました。ビルド時と実行時の初期化の利点については、ネイティブイメージでのクラス初期化で説明されていますが、要するに、ビルド時の初期化は、正しく使用すると、全体のファイルサイズを大幅に削減し、アプリケーションの実行時間を短縮できます。