ネイティブ・イメージの互換性および最適化ガイド

ネイティブ・イメージでは、Java HotSpot VMのユーザーが使い慣れた方法とは異なる方法でJavaプログラムを実行します。イメージのビルド時イメージの実行時を区別します。イメージのビルド時には、静的分析により、アプリケーションのエントリ・ポイントからアクセス可能なすべてのメソッドが検索されます。これらのメソッドのみがネイティブ・イメージに事前にコンパイルされます。最適化モデルの相違から、Javaプログラムをネイティブ・イメージにコンパイルした場合、プログラムの動作が多少異なる可能性があります。

ネイティブ・イメージは、アプリケーションのメモリー・フットプリントおよび起動時間を縮小する最適化です。これには、イメージのビルド時にすべてのコードがわかっている(つまり、実行時に新しいコードがロードされない)という閉世界仮説が必要です。ほとんどの最適化と同様に、すべてのアプリケーションがその最適化のために修正できるわけではありません。アプリケーションが最適化できない場合、Java HotSpot VMを起動するいわゆるフォールバック・イメージが生成されます。つまり、実行にはJDKが必要です。

クラス・メタデータ機能(構成が必要)

次の機能では、通常、クローズドワールド最適化を使用するためにイメージのビルド時に構成が必要になります。この構成により、ネイティブ・イメージ・バイナリで使用される領域が必要最小限に抑えられます。

イメージのビルド時に構成を指定せずに次の機能のいずれかを使用すると、フォールバック・イメージが生成されます。

動的クラス・ロード

イメージの実行時に名前でアクセスされるクラスは、イメージのビルド時に列挙する必要があります。たとえば、Class.forName("myClass”)をコールする場合、構成ファイルmyClassが含まれている必要があります。構成ファイルを使用したとしても、動的クラス・ロードを要求されたクラスが含まれていない場合は、クラスがクラスパスに見つからないか、アクセスできなかった場合と同様に、ClassNotFoundExceptionがスローされます。

リフレクション

このカテゴリには、クラスのメソッドとフィールドのリスト、メソッドの呼出しとフィールドへのリフレクティブなアクセス、およびパッケージjava.lang.reflect内の他のクラスの使用が含まれます。

リフレクションを介してアクセス可能にする個々のクラス、メソッドおよびフィールドは事前に認識されている必要があります。ネイティブ・イメージでは、リフレクションAPIのコールを検出する静的分析を通じて、これらの要素の解決が試みられます。分析が失敗した場合、実行時にリフレクティブにアクセスされるプログラム要素は、ネイティブ・イメージの生成時に構成ファイルで指定するか、FeatureからRuntimeReflectionを使用して指定する必要があります。詳細は、リフレクション・サポート・ガイドを参照してください。

リフレクションは、ネイティブ・イメージの生成時にクラス・イニシャライザなどで制限なく使用できます。

動的プロキシ

このカテゴリには、java.lang.reflect.Proxy APIを使用した動的プロキシ・クラスの生成および動的プロキシ・クラスのインスタンスの割当てが含まれます。動的クラス・プロキシは、バイトコードが事前に生成されているかぎり、クローズドワールド最適化でサポートされます。つまり、イメージのビルド時に、動的プロキシを定義するインタフェースのリストが認識されている必要があります。ネイティブ・イメージでは、java.lang.reflect.Proxy.newProxyInstance(ClassLoader, Class<?>[], InvocationHandler)およびjava.lang.reflect.Proxy.getProxyClass(ClassLoader, Class<?>[])のコールをインターセプトし、インタフェースのリストの自動判別を試みる単純な静的分析が採用されています。分析が失敗した場合は、構成ファイルでインタフェースのリストを指定できます。詳細は、動的プロキシ・サポート・ガイドを参照してください。

JNI (Java Native Interface)

ネイティブ・コードでは、JavaコードでリフレクションAPIを使用する場合と同様の方法で、名前でJavaオブジェクト、クラス、メソッドおよびフィールドにアクセスできます。同じ理由で、JNIを介して名前でアクセスされるJavaアーティファクトは、ネイティブ・イメージの生成時に構成ファイルで指定する必要があります。詳細は、JNI実装ガイドを参照してください。

また、ネイティブ・イメージは、JNIとは別に、JNIよりもはるかに単純でオーバーヘッドの少ない独自のネイティブ・インタフェースにも対応しています。これにより、JavaとCの間のコール、およびJavaコードからのCデータ構造へのアクセスが可能になります。ただし、CコードからJavaデータ構造にアクセスすることはできません。詳細は、パッケージorg.graalvm.nativeimage.cとそのサブパッケージのJavaDocを参照してください。

シリアライゼーション

Javaシリアライゼーションを機能させるには、クラス・メタデータ情報が必要になり、ネイティブ・イメージの生成時に構成ファイルで指定する必要があります。ただし、Javaシリアライゼーションはセキュリティ脆弱性の永続的な原因となっています。Javaアーキテクトは、既存のシリアライゼーション・メカニズムを新しいメカニズムに置き換えて、近い将来これらの問題を回避することを発表しました。

クローズドワールド最適化と互換性のない機能

一部のJava機能は、クローズドワールド最適化ではまだサポートされておらず、使用するとフォールバック・イメージが生成されます。

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

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

Javaラムダ式や文字列連結など、javacによって生成されるinvokedynamicユース・ケースについては、イメージの実行時にコール対象のメソッドが変更されないという理由でサポートされています。

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

信頼性の低いコードを同じプロセス内の信頼性の高いコードから分離する方法として、Javaセキュリティ・マネージャは推奨されなくなりました。その理由としては、一般的なハードウェア・アーキテクチャのほとんどは、セキュリティ・マネージャによって制限されたデータへのアクセスに対するサイドチャネル攻撃の影響を受けやすいことがあげられます。このような場合は、個別のプロセスを使用することをお薦めします。

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

ネイティブ・イメージでは、一部のJava機能がJava HotSpot VMとは異なる方法で実装されます。

シグナル・ハンドラ

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

デフォルトのシグナル・ハンドラを登録するには、native-imageビルダーに--install-exit-handlersオプションを渡します。このオプションでは、JVMと同じシグナル・ハンドラが提供されます。

クラス・イニシャライザ

デフォルトでは、クラスはイメージの実行時に初期化されます。これにより互換性が保証されますが、一部の最適化が制限されます。起動を高速化し、ピーク・パフォーマンスを向上させるには、イメージのビルド時にクラスを初期化することをお薦めします。クラスの初期化動作は、特定のクラスとパッケージまたはすべてのクラスに対してオプション--initialize-at-build-timeまたは--initialize-at-run-timeを使用して調整できます。詳細は、native-image --helpを参照してください。JDKクラス・ライブラリのクラスは自動的に処理されるため、ユーザーによる特別な考慮は必要ありません。

ネイティブ・イメージ・ユーザーは、イメージのビルド時にクラスを初期化すると、既存のコードの特定の仮定が壊れる可能性があることに注意する必要があります。たとえば、クラス・イニシャライザにロードされたファイルは、イメージのビルド時とイメージの実行時で同じ場所に配置されるとはかぎりません。また、ファイル記述子や実行中のスレッドなどの特定のオブジェクトは、ネイティブ・イメージ・バイナリに格納しないでください。イメージのビルド時にこのようなオブジェクトにアクセス可能な場合、イメージの生成はエラーで失敗します。

ファイナライザ

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

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

スレッド

ネイティブ・イメージでは、java.lang.Threadに対してThread.stop()などの長らく非推奨のままのメソッドが実装されることはありません。

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

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

デバッグおよびモニタリング

Javaには、JVMTIなど、Java実装でJavaプログラムのデバッグおよびモニタリングに使用できるオプション仕様がいくつかあります。これらは、ほとんどのネイティブ・イメージでは発生しないコンパイルなどのイベントについて、実行時にVMをモニターする場合に役立ちます。これらのインタフェースは、実行時にJavaバイトコードが使用可能であることを前提としています。これは、クローズドワールド最適化でビルドされたネイティブ・イメージには当てはまりません。ネイティブ・イメージ・ビルダーではネイティブ・バイナリが生成されるため、ユーザーは、Javaを対象としたツールではなく、ネイティブ・デバッガおよびモニタリング・ツール(GDBやVTuneなど)を使用する必要があります。JVMTIおよびその他のバイトコードベースのツールは、ネイティブ・イメージではサポートされていません。