ネイティブ・イメージの基本
ネイティブ・イメージはJavaで記述され、スタンドアロン・バイナリ(実行可能ファイルまたは共有ライブラリ)を生成するための入力としてJavaバイトコードを取得します。バイナリの生成プロセス中、ネイティブ・イメージはユーザー・コードを実行できます。最後に、ネイティブ・イメージは、コンパイルされたユーザー・コード、Javaランタイムの一部(ガベージ・コレクタ、スレッド・サポートなど)、およびコード実行の結果をバイナリにリンクします。
このバイナリをネイティブ実行可能ファイルまたは単にネイティブ・イメージと呼びます。バイナリを生成するユーティリティをnative-image
ビルダーまたはnative-image
ジェネレータと呼びます。
ネイティブ・イメージのビルド中に実行されるコードとネイティブ・イメージの実行中に実行されるコードを明確に区別するために、2つの違いをビルド時および実行時と呼びます。
最小イメージを生成するために、ネイティブ・イメージは静的分析と呼ばれるプロセスを使用します。
目次
ビルド時と実行時
イメージのビルド中に、ネイティブ・イメージがユーザー・コードを実行する場合があります。このコードには、クラスの静的フィールドに値を書き込むなどの副作用があります。このコードはビルド時に実行されると言います。このコードによって静的フィールドに書き込まれる値は、イメージ・ヒープに保存されます。実行時は、バイナリが実行されたときのコードおよび状態を表します。
この2つの概念の違いを確認する最も簡単な方法は、構成可能なクラス初期化を使用することです。Javaでは、クラスが最初に使用されたときに初期化されます。ビルド時に使用されるすべてのJavaクラスは、ビルド時の初期化と呼ばれます。クラスをロードするだけでは、必ずしも初期化されないことに注意してください。ビルド時の初期化クラスの静的クラス・イニシャライザは、イメージ・ビルドを実行しているJVMで実行されます。ビルド時にクラスが初期化されると、その静的フィールドは生成されたバイナリに保存されます。実行時に、このようなクラスをはじめて使用しても、クラスの初期化はトリガーされません。
ユーザーは、ビルド時にクラス初期化を様々な方法でトリガーできます:
--initialize-at-build-time=<class>
をnative-image
ビルダーに渡す。- ビルド時の初期化クラスの静的イニシャライザでクラスを使用する。
ネイティブ・イメージは、イメージのビルド時に頻繁に使用されるJDKクラスを初期化します(java.lang.String
、java.util.**
など)。ビルド時のクラスの初期化はエキスパート機能です。すべてのクラスがビルド時の初期化に適しているわけではありません。
次の例は、ビルド時の実行コードと実行時の実行コードの違いを示しています:
public class HelloWorld {
static class Greeter {
static {
System.out.println("Greeter is getting ready!");
}
public static void greet() {
System.out.println("Hello, World!");
}
}
public static void main(String[] args) {
Greeter.greet();
}
}
コードをHelloWorld.javaという名前のファイルに保存した後、JVMでアプリケーションをコンパイルして実行します:
javac HelloWorld.java
java HelloWorld
Greeter is getting ready!
Hello, World!
次に、そのネイティブ・イメージをビルドし、実行します:
native-image HelloWorld
========================================================================================================================
GraalVM Native Image: Generating 'helloworld' (executable)...
========================================================================================================================
...
Finished generating 'helloworld' in 14.9s.
./helloworld
Greeter is getting ready!
Hello, World!
HelloWorld
が起動し、Greeter.greet
が呼び出されました。これにより、Greeter
が初期化され、メッセージGreeter is getting ready!
が出力されました。ここでは、Greeter
のクラス・イニシャライザはイメージの実行時に実行されると言います。
ビルド時にGreeter
を初期化するようにnative-image
に指示するとどうなりますか。
native-image HelloWorld --initialize-at-build-time=HelloWorld\$Greeter
========================================================================================================================
GraalVM Native Image: Generating 'helloworld' (executable)...
========================================================================================================================
Greeter is getting ready!
[1/7] Initializing... (3.1s @ 0.15GB)
Version info: 'GraalVM dev Java 11 EE'
Java version info: '11.0.15+4-jvmci-22.1-b02'
C compiler: gcc (linux, x86_64, 9.4.0)
Garbage collector: Serial GC
...
Finished generating 'helloworld' in 13.6s.
./helloworld
Hello, World!
イメージのビルド中にGreeter is getting ready!
が出力されました。Greeter
のクラス・イニシャライザはイメージのビルド時に実行されると言います。実行時に、HelloWorld
がGreeter.greet
を起動したとき、Greeter
はすでに初期化されています。イメージのビルド中に初期化されるクラスの静的フィールドは、イメージ・ヒープに格納されます。
ネイティブ・イメージ・ヒープ
ネイティブ・イメージ・ヒープ(イメージ・ヒープとも呼ばれる)には、次のものが含まれます:
- アプリケーション・コードからアクセス可能なイメージのビルド中に作成されたオブジェクト。
- ネイティブ・イメージで使用されるクラスの
java.lang.Class
オブジェクト。 - メソッド・コードに埋め込まれたオブジェクト定数。
ネイティブ・イメージが起動すると、バイナリから初期イメージ・ヒープがコピーされます。
イメージ・ヒープにオブジェクトを含める1つの方法は、ビルド時にクラスを初期化することです:
class Example {
private static final String message;
static {
message = System.getProperty("message");
}
public static void main(String[] args) {
System.out.println("Hello, World! My message is: " + message);
}
}
ここで、JVMでアプリケーションをコンパイルして実行します:
javac Example.java
java -Dmessage=hi Example
Hello, World! My message is: hi
java -Dmessage=hello Example
Hello, World! My message is: hello
java Example
Hello, World! My message is: null
次に、Example
クラスがビルド時に初期化されるネイティブ・イメージをビルドした場合の動作を確認します:
native-image Example --initialize-at-build-time=Example -Dmessage=native
================================================================================
GraalVM Native Image: Generating 'example' (executable)...
================================================================================
...
Finished generating 'example' in 19.0s.
./example
Hello, World! My message is: native
./example -Dmessage=aNewMessage
Hello, World! My message is: native
Example
クラスのクラス・イニシャライザは、イメージのビルド時に実行されました。これにより、message
フィールドにString
オブジェクトが作成され、イメージ・ヒープ内に格納されます。
静的分析
静的分析は、アプリケーションで使用されるプログラム要素(クラス、メソッド、およびフィールド)を決定するプロセスです。これらの要素は、アクセス可能コードとも呼ばれます。分析自体には次の2つの部分があります:
- メソッドのバイトコードをスキャンして、そこからアクセス可能な他の要素を確認します。
- ネイティブ・イメージ・ヒープ(つまり、静的フィールド)のルート・オブジェクトをスキャンして、そこからアクセス可能なクラスを特定します。アプリケーションのエントリ・ポイント(つまり、
main
メソッド)から開始します。新しく検出された要素は、後続のスキャンで要素のアクセス可能性が変化しなくなるまで繰り返しスキャンされます。
最終イメージには、アクセス可能な要素のみが含まれます。ネイティブ・イメージが作成されると、クラス・ロードなどにより、実行時に新しい要素を追加できなくなります。この制約を閉世界仮説と呼びます。