ネイティブイメージの基礎

ネイティブイメージはJavaで記述されており、Javaバイトコードを入力としてスタンドアロンバイナリ(実行可能ファイルまたは共有ライブラリ)を生成します。バイナリを生成する過程で、ネイティブイメージはユーザーコードを実行できます。最後に、ネイティブイメージはコンパイルされたユーザーコード、Javaランタイムの一部(ガベージコレクタ、スレッドサポートなど)、およびコード実行の結果をバイナリにリンクします。

このバイナリをネイティブ実行可能ファイルまたはネイティブイメージと呼びます。バイナリを生成するユーティリティを`native-image`ビルダーまたは`native-image`ジェネレーターと呼びます。

ネイティブイメージのビルド中に実行されるコードと、ネイティブイメージの実行中に実行されるコードを明確に区別するために、この2つの違いをビルド時実行時と呼びます。

最小限のイメージを生成するために、ネイティブイメージは静的解析と呼ばれるプロセスを採用しています。

目次 #

ビルド時と実行時 #

イメージのビルド中に、ネイティブイメージはユーザーコードを実行する場合があります。このコードは、クラスの静的フィールドに値を書き込むなど、副作用を持つ可能性があります。このコードは*ビルド時*に実行されると言います。このコードによって静的フィールドに書き込まれた値は、イメージヒープに保存されます。*実行時*とは、バイナリが実行されるときのコードと状態を指します。

これら2つの概念の違いを確認する最も簡単な方法は、設定可能なクラス初期化です。Javaでは、クラスは最初に使用されるときに初期化されます。ビルド時に使用されるすべてのJavaクラスは、**ビルド時初期化**されていると言います。クラスをロードするだけでは、必ずしも初期化されるとは限りません。ビルド時初期化クラスの静的クラス初期化子は、**イメージビルドを実行しているJVM上**で実行されます。クラスがビルド時に初期化されると、その静的フィールドは生成されたバイナリに保存されます。実行時に、そのようなクラスを初めて使用しても、クラスの初期化はトリガーされません。

ユーザーは、さまざまな方法でビルド時にクラスの初期化をトリガーできます。

  • `native-image`ビルダーに`--initialize-at-build-time=<class>`を渡すことによります。
  • ビルド時初期化クラスの静的初期化子でクラスを使用することによります。

ネイティブイメージは、イメージビルド時に頻繁に使用される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`のクラス初期化子が*イメージ実行時*に実行されたと言います。

`native-image`にビルド時に`Greeter`を初期化するように指示するとどうなるでしょうか?

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`メソッド)から開始されます。新しく検出された要素は、要素の到達可能性にそれ以上の変更がなくなるまで反復的にスキャンされます。

最終的なイメージには、**到達可能な**要素のみが含まれます。ネイティブイメージがビルドされると、実行時に新しい要素(クラスのロードなど)を追加することはできません。この制約を**閉鎖世界仮説**と呼びます。

参考文献 #

お問い合わせ