ネイティブイメージ互換性ガイド

ネイティブイメージは、従来のJava仮想マシン(VM)とは異なる方法でJavaアプリケーションをコンパイルします。 **ビルド時**と**実行時**を区別します。イメージビルド時に、native-imageビルダーは静的分析を実行して、アプリケーションのエントリポイントから到達可能なすべてのメソッドを見つけます。ビルダーは、これらのメソッド(そしてこれらのメソッドのみ)を実行可能なバイナリにコンパイルします。この異なるコンパイルモデルのため、Javaアプリケーションはネイティブイメージにコンパイルされると、動作が多少異なる場合があります。

ネイティブイメージは、アプリケーションのメモリフットプリントと起動時間を削減するための最適化を提供します。このアプローチは、すべてのコードがビルド時に既知であるという「閉世界仮説」に依存しています。つまり、実行時に新しいコードはロードされません。ほとんどの最適化と同様に、すべてのアプリケーションがこのアプローチに適しているわけではありません。 native-imageビルダーがビルド時にアプリケーションを最適化できない場合、Java VMを実行するために必要な、いわゆる「フォールバックファイル」が生成されます。ビルド時と実行時にJavaアプリケーションで何が起こるかについての詳細な説明については、ネイティブイメージの基礎を確認することをお勧めします。

メタデータを必要とする機能 #

閉世界仮説に適応するには、次のJava機能は、一般的にビルド時にnative-imageに渡すメタデータを必要とします。このメタデータにより、ネイティブイメージが必要な最小限のスペースを使用することが保証されます。

最近、GitHubで共有到達可能性メタデータを公開することにより、ネイティブイメージと最も一般的なJavaライブラリとの互換性が強化されました。ユーザーは、サードパーティの依存関係のメタデータを維持する負担を共有し、再利用できます。詳細については、到達可能性メタデータを参照してください。

閉世界仮説と互換性のない機能 #

一部のJava機能は、閉世界仮説ではまだサポートされておらず、使用するとフォールバックファイルが生成されます。

invokedynamicバイトコードとメソッドハンドル #

閉世界仮説では、呼び出されるすべてのメソッドとその呼び出しサイトが既知である必要があります。 invokedynamicメソッドとメソッドハンドルは、実行時に呼び出しを導入したり、呼び出されるメソッドを変更したりできます。

実行時に呼び出されるメソッドを変更しないため、Javaラムダ式や文字列連結などのjavacによって生成されたinvokedynamicユースケースはサポートされています。

ネイティブイメージでは動作が異なる可能性のある機能 #

ネイティブイメージは、Java VMとは異なる方法で一部のJava機能を実装しています。

セキュリティマネージャー #

起動時に-Djava.security.managerを介してセキュリティマネージャーが設定されている場合でも、java.lang.System#getSecurityManager()は常にnullを返します。

プログラムの起動時に-Djava.security.managerdisallow以外に設定されている場合、null以外の引数で呼び出されたjava.lang.System#setSecurityManager(SecurityManager)は、java.lang.SecurityExceptionをスローします。

シグナルハンドラ #

シグナルハンドラの登録には、シグナルを処理してシャットダウンフックを呼び出す新しいスレッドを開始する必要があります。デフォルトでは、ユーザーが明示的に登録しない限り、ネイティブイメージのビルド時にシグナルハンドラは登録されません。たとえば、共有ライブラリのビルド時にデフォルトのシグナルハンドラを登録することはお勧めしませんが、Dockerコンテナなどのコンテナ化環境用のネイティブ実行可能ファイルをビルドするときにシグナルハンドラを含めることが望ましいです。

デフォルトのシグナルハンドラを登録するには、--install-exit-handlersオプションをnative-imageビルダーに渡します。このオプションを使用すると、Java VMと同じシグナルハンドラが得られます。

クラスイニシャライザ #

デフォルトでは、クラスは実行時に初期化されます。これは互換性を確保しますが、一部の最適化が制限されます。より高速な起動とより優れたピークパフォーマンスを実現するには、ビルド時にクラスを初期化する方が適切です。クラスの初期化動作は、特定のクラスとパッケージ、またはすべてのクラスに対して、オプションの--initialize-at-build-timeまたは--initialize-at-run-timeを使用して指定できます。JDKクラスライブラリのメンバーであるクラスは、デフォルトで初期化されます。

**注意**: ビルド時のクラス初期化は、既存のコードの特定の仮定を壊す可能性があります。たとえば、クラスイニシャライザでロードされたファイルは、ビルド時と実行時で同じ場所にあるとは限りません。また、ファイル記述子や実行中のスレッドなどの特定のオブジェクトは、ネイティブ実行可能ファイルに格納しないでください。このようなオブジェクトがビルド時に到達可能な場合、native imageビルダーはエラーで失敗します。

詳細については、ネイティブイメージでのクラスの初期化を参照してください。

ファイナライザ #

Java基本クラスjava.lang.Objectは、メソッドfinalize()を定義します。ガベージコレクションによってオブジェクトへの参照がなくなったと判断されたときに、ガベージコレクターによってオブジェクトで呼び出されます。サブクラスは、finalize()メソッドをオーバーライドして、システムリソースを破棄したり、その他のクリーンアップ操作を実行したりできます。

ファイナライザはJava SE 9以降非推奨となっています。実装が複雑で、セマンティクスが適切に設計されていません。たとえば、ファイナライザは、静的フィールドに参照を格納することにより、オブジェクトが再び到達可能になる可能性があります。そのため、ファイナライザは呼び出されません。ファイナライザを弱参照と参照キューに置き換えることをお勧めします。

スレッド #

ネイティブイメージは、java.lang.ThreadThread.stop()などの廃止されたメソッドを実装していません。

安全でないメモリアクセス #

クラスがビルド時に初期化される場合、sun.misc.Unsafeを使用してアクセスされるフィールドは、静的分析のためにそのようにマークする必要があります。ほとんどの場合、これは自動的に行われます。static finalフィールドに格納されているフィールドオフセットは、ホストされた値(native imageビルダーが実行されているJava VMのフィールドオフセット)からネイティブ実行可能ファイルの値に自動的に書き換えられ、その書き換えの一部としてフィールドはUnsafeアクセスとしてマークされます。非標準パターンでは、アノテーションRecomputeFieldValueを使用してフィールドオフセットを手動で再計算できます。

デバッグと監視 #

Javaには、JVMTIなど、Javaプログラムのデバッグと監視に使用できるオプションの仕様がいくつかあります。これらは、たとえば、ほとんどのネイティブイメージでは発生しないコンパイルなどのイベントについて、実行時にJava VMを監視するのに役立ちます。これらのインターフェースは、Javaバイトコードが実行時に使用可能であるという仮定に基づいて構築されていますが、閉世界最適化で構築されたネイティブイメージの場合はそうではありません。 native-imageビルダーはネイティブ実行可能ファイルを生成するため、ユーザーはJava向けのツールではなく、ネイティブデバッガーと監視ツール(GDBやVTuneなど)を使用する必要があります。JVMTIおよびその他のバイトコードベースのツールは、ネイティブイメージではサポートされていません。

Linux AArch64アーキテクチャの制限

ほとんどすべてのネイティブイメージ機能は、以下に説明する制限を除いて、Linux AArch64アーキテクチャでサポートされています。

  • -R:[+|-]WriteableCodeCache:無効にする必要があります。
  • --libc=<value>muslはサポートされていません。
  • --gc=<value>:G1ガベージコレクター(G1)はサポートされていません。

native-imageビルダーのオプションのリストはこちらにあります。

お問い合わせ