ネイティブ・イメージ

ネイティブ・イメージは、Javaコードをスタンドアロンの実行可能ファイル(ネイティブ・イメージと呼ばれます)に事前にコンパイルするテクノロジです。この実行可能ファイルには、アプリケーション・クラス、その依存性からのクラス、ランタイム・ライブラリ・クラス、およびJDKから静的にリンクされたネイティブ・コードが含まれています。Java VMでは実行されませんが、Substrate VMと呼ばれる別のランタイム・システムからのメモリー管理やスレッド・スケジューリングなどの必要なコンポーネントが含まれています。Substrate VMは、ランタイム・コンポーネント(デオプティマイザ、ガベージ・コレクタ、スレッド・スケジューリングなど)の名前です。生成されるプログラムは、JVMと比較して起動時間が短く、ランタイム・メモリー・オーバーヘッドが少なくなります。

ネイティブ・イメージ・ビルダーまたはnative-imageは、アプリケーションのすべてのクラスとその依存性(JDKからのものを含む)を処理するユーティリティです。これらのデータを静的に分析することで、アプリケーションの実行中にアクセス可能なクラスおよびメソッドが判別されます。次に、このようなアクセス可能なコードおよびデータが事前にコンパイルされて、特定のオペレーティング・システムおよびアーキテクチャのネイティブ実行可能ファイルが作成されます。このプロセス全体は、Javaソース・コードからバイトコードへのコンパイルと明確に区別するために、イメージのビルド(またはイメージ・ビルド時間)と呼ばれます。

ネイティブ・イメージは、JVMベースの言語(Java、Scala、Clojure、Kotlinなど)をサポートしています。生成されるイメージは、オプションで、JavaScript、Ruby、R、Pythonなどの動的言語を実行できます。ポリグロット埋込みも事前にコンパイルできます。アプリケーションで使用されるゲスト言語をnative-imageに通知するには、ゲスト言語ごとに--language:<languageId>を指定します(たとえば、--language:js)。

ライセンス

ネイティブ・イメージ・テクノロジは、GraalVMに個別にインストール可能なものとして配布されています。GraalVM Community Editionのネイティブ・イメージはクラスパス例外を持つGPL 2に基づいてライセンスされています。

GraalVM Enterprise Editionのネイティブ・イメージは、アーリー・アダプタ機能として使用可能です。アーリー・アダプタ機能は、継続的な開発、テストおよび変更の対象となります。詳細は、GraalVM Enterprise EditionのOracle Technology Networkライセンス契約を確認してください。

ネイティブ・イメージのインストール

ネイティブ・イメージは、GraalVMアップデータ・ツールを使用してGraalVMに追加できます。

次のコマンドを実行して、ネイティブ・イメージをインストールします:

gu install native-image

この追加ステップの後、native-image実行可能ファイルが$JAVA_HOME/binディレクトリで使用可能になります。

前述のコマンドでは、GraalVM Communityユーザー用のGitHubカタログからネイティブ・イメージがインストールされます。GraalVM Enterpriseのユーザーの場合、手動インストールが必要です。

前提条件

コンパイルの場合、native-imageはローカル・ツールチェーンによって異なります。使用しているOSで使用可能なパッケージ・マネージャを使用して、glibc-develzlib-devel (Cライブラリおよびzlibのヘッダー・ファイル)およびgccをインストールします。一部のLinuxディストリビューションでは、さらにlibstdc++-staticが必要な場合があります。

Oracle Linuxでは、yumパッケージ・マネージャを使用します:

sudo yum install gcc glibc-devel zlib-devel

オプションのリポジトリ(Oracle Linux 7ではol7_optional_latest、Oracle Linux 8ではol8_codeready_builder)が有効になっているかぎり、libstdc++-staticをインストールできます。

Ubuntu Linuxでは、apt-getパッケージ・マネージャを使用します:

sudo apt-get install build-essential libz-dev zlib1g-dev

その他のLinuxディストリビューションでは、dnfパッケージ・マネージャを使用します:

sudo dnf install gcc glibc-devel zlib-devel libstdc++-static

macOSでは、xcodeを使用します:

xcode-select --install

Windowsでネイティブ・イメージを使用するための前提条件

Windowsでネイティブ・イメージの使用を開始するには、Visual StudioおよびMicrosoft Visual C++ (MSVC)をインストールします。2つのインストール・オプションがあります: * Windows 10 SDKを使用したVisual Studioビルド・ツールのインストール * Windows 10 SDKを使用したVisual Studioのインストール

Visual Studio 2017バージョン15.9以降を使用できます。

最後に、Windowsでは、native-imageビルダーは、x64ネイティブ・ツールのコマンド・プロンプトから実行した場合にのみ機能します。x64ネイティブ・ツールのコマンド・プロンプトを開始するコマンドは、Visual Studioビルド・ツールのみをインストールしている場合と、VS 2019をフル・インストールしている場合では異なります。ステップごとの手順は、このリンクを参照してください。

ネイティブ・イメージのビルド

ネイティブ・イメージは、スタンドアロンの実行可能ファイル(デフォルト)または共有ライブラリ(「共有ライブラリのビルド」を参照)としてビルドできます。イメージが有用となるには、少なくとも1つのエントリ・ポイント・メソッドが必要です。スタンドアロンの実行可能ファイルの場合、ネイティブ・イメージは、コマンドライン引数を文字列の配列として取得するシグネチャを持つJavaメイン・メソッドをサポートします:

public static void main(String[] arg) { /* ... */ }

実行可能イメージには、コールバックやAPIの実装用など、任意の数のエントリ・ポイントを含めることができます。

現在の作業ディレクトリでJavaクラス・ファイルのネイティブ・イメージをビルドするには、次を使用します:

native-image [options] class [imagename] [options]

JARファイルのネイティブ・イメージをビルドするには、次を使用します:

native-image [options] -jar jarfile [imagename] [options]

native-imageコマンドでは、javaランチャの一般的なオプションである、-cpを使用してすべてのクラスのクラス・パスを指定する必要があり、その後に指定するディレクトリまたはJARファイルのリストは、LinuxおよびmacOSプラットフォームでは:、Windowsでは;で区切ります。メイン・メソッドを含むクラスの名前が最後の引数となり、そのようにしない場合は、-jarを使用して、マニフェスト内でメイン・メソッドを指定するJARファイルを指定できます。

例として、再帰を使用して文字列を逆にする次の小さいJavaプログラムを考えてみます:

public class Example {

    public static void main(String[] args) {
        String str = "Native Image is awesome";
        String reversed = reverseString(str);
        System.out.println("The reversed string is: " + reversed);
    }

    public static String reverseString(String str) {
        if (str.isEmpty())
            return str;
        return reverseString(str.substring(1)) + str.charAt(0);
    }
}

コンパイルして、Javaクラスからネイティブ・イメージをビルドします:

javac Example.java
native-image Example

ネイティブ・イメージ・ビルダーは、現在の作業ディレクトリでExampleクラスをスタンドアロンの実行可能ファイルexampleに事前にコンパイルします。実行可能ファイルを実行します:

./example

ネイティブ・イメージ・ビルダーの別のオプションとして、--install-exit-handlersが役立つ場合があります。共有ライブラリをビルドするときにデフォルトのシグナル・ハンドラを登録することはお薦めしません。ただし、Dockerコンテナなどのコンテナ化された環境のネイティブ・イメージをビルドする場合は、シグナル・ハンドラを含めることが適切です。--install-exit-handlersオプションでは、JVMと同じシグナル・ハンドラが提供されます。

より複雑な例については、ネイティブ・イメージ生成またはJavaおよびKotlinアプリケーションの事前コンパイルのページを参照してください。

共有ライブラリのビルド

ネイティブ・イメージをJavaクラス・ファイルの共有ライブラリとしてビルドするには、ネイティブ・イメージ・ビルダーに--sharedを渡します。作成された共有ライブラリには、指定されたJavaクラスのメイン・メソッドがエントリ・ポイント・メソッドとして含まれています。

native-image class [libraryname] --shared

ネイティブ・イメージをJARファイルの共有ライブラリとしてビルドするには、次のようにします:

native-image -jar jarfile [libraryname] --shared

ノート: メイン・クラスを指定しない共有ライブラリをビルドする場合は、-H:Name=フラグを追加して、-H:Name=librarynameのようにライブラリ名を指定する必要があります。

前の項で説明したように、ネイティブ・イメージが有用となるには、少なくとも1つのエントリ・ポイント・メソッドが必要です。共有ライブラリのために、ネイティブ・イメージでは、エクスポートしてCからコール可能なエントリ・ポイント・メソッドを指定する@CEntryPoint注釈を提供します。エントリ・ポイント・メソッドは静的である必要があり、オブジェクト以外のパラメータおよび戻り型のみを持つことができ、これにはJavaプリミティブだけでなくWord型(ポインタを含む)も含まれます。エントリ・ポイント・メソッドのパラメータのいずれかは、IsolateThreadまたはIsolate型である必要があります。このパラメータは、コールに対して現在のスレッドの実行コンテキストを指定します。

たとえば:

@CEntryPoint static int add(IsolateThread thread, int a, int b) {
    return a + b;
}

共有ライブラリをビルドすると、追加のCヘッダー・ファイルが生成されます。このヘッダー・ファイルにはC APIの宣言が含まれ、これを使用すると分離の作成とCコードからのスレッドのアタッチ、ソース・コード内の各エントリ・ポイントの宣言が可能です。前述の例で生成されるC宣言は次のとおりです:

int add(graal_isolatethread_t* thread, int a, int b);

共有ライブラリ・イメージと実行可能イメージはいずれも同様で、コールバックやAPIの実装など、任意の数のエントリ・ポイントを持つことができます。

イメージの生成に使用されるGraalVMのバージョンを判別する方法

空のメイン・メソッドを含むJavaクラス・ファイルEmptyHello.classがあり、ネイティブ・イメージ・ビルダーを使用して空の共有オブジェクトemptyhelloを生成したものとします:

native-image -cp hello EmptyHello
[emptyhello:11228]    classlist:     149.59 ms
...

PATH環境変数に設定されているGraalVMディストリビューションがわからない場合は、ネイティブ・イメージのコンパイルにCommunity EditionとEnterprise Editionのどちらが使用されたかを判別するにはどうすればよいでしょうか。次のコマンドを実行します:

strings emptyhello | grep com.oracle.svm.core.VM

予期される出力は次のとおりです:

com.oracle.svm.core.VM GraalVM <version> Java 11 EE

ノート: GraalVM Community Editionで解釈またはコンパイルされたPythonソース・コードまたはLLVMビットコードは、GraalVM Enterprise Editionを使用して解釈またはコンパイルされた同じコードと同じセキュリティ特性を持ちません。それそれのイメージには、イメージをビルドするために使用されたベースのバージョンおよびバリアント(CommunityまたはEnterprise)を確認できるGraalVM文字列が埋め込まれています。次のコマンドは、イメージからその情報を問い合せます:

strings <path to native-image exe or shared object> | grep com.oracle.svm.core.VM

次に出力例を示します:

com.oracle.svm.core.VM.Target.LibC=com.oracle.svm.core.posix.linux.libc.GLibC
com.oracle.svm.core.VM.Target.Platform=org.graalvm.nativeimage.Platform$LINUX_AMD64
com.oracle.svm.core.VM.Target.StaticLibraries=liblibchelper.a|libnet.a|libffi.a|libextnet.a|libnio.a|libjava.a|libfdlibm.a|libzip.a|libjvm.a
com.oracle.svm.core.VM=GraalVM <version> Java 11
com.oracle.svm.core.VM.Target.Libraries=pthread|dl|z|rt
com.oracle.svm.core.VM.Target.CCompiler=gcc|redhat|x86_64|10.2.1

イメージがOracle GraalVM Enterprise Editionでビルドされた場合は、かわりに次の内容が出力に含まれます:

com.oracle.svm.core.VM=GraalVM <version> Java 11 EE

Ahead-of-Timeコンパイルの制限事項

ごく一部のJava機能は、Ahead-of-Timeコンパイルの影響を受けにくいためにパフォーマンス上の利点がありません。高度に最適化されたネイティブ実行可能ファイルをビルドできるようにするために、GraalVMでは、閉世界仮説を必要とする積極的な静的分析が実行されます。そのため、実行時にアクセス可能なすべてのクラスとすべてのバイトコードがビルド時に認識される必要があります。したがって、Ahead-of-Timeコンパイル中に使用可能でなかった新しいデータはロードできません。続いて、GraalVMネイティブ・イメージの互換性および最適化を参照してください。