Graal VM ネイティブイメージにおける制限事項

f:id:Naotsugu:20210908232429p:plain

以下のページの意訳です。

github.com


Native Image Compatibility and Optimization Guide

ネイティブイメージは、Java HotSpot VM とは異なる方法で実行されます。ネイティブイメージのビルド時には、静的解析により、アプリケーションのエントリポイントから到達可能な全てのメソッドを検出し、これらのメソッドのみがネイティブイメージに(ahead-of-time)コンパイルされます。実行時にネイティブコードにコンパイルされて最適化される通常の Java とは、最適化モデルが異なります。

ネイティブイメージの作成は、アプリケーションのメモリフットプリントと起動時間を削減する最適化で、closed-world が前提となります。つまり、イメージの構築時に全てのコードが既知となっており、実行時に新しいコードが読み込まれることはない ということです。ほとんどの最適化と同様に、すべてのアプリケーションがこの最適化に適しているわけではありません。アプリケーションが最適化できない場合には(ネイティブイメージへのビルドが上手くいかない部分がある場合、上手くいかない部分は) Java HotSpot VM を起動する、いわゆるフォールバックイメージが生成されます。このケースでは実行にJDK が必要になります。

なお、ネイティブイメージのビルド時に -no-fallback を付けることでフォールバックを禁止することができます。


Class Metadata Features (Require Configuration)

以下に示す機能において、closed-world の最適化を使用するためには、イメージのビルド時にコンフィグレーションが必要となります。コンフィグレーションを行うことで、ネイティブイメージのバイナリは最小限の構成となります。適切な設定を行わない場合には、フォールバックイメージが生成されます。

Dynamic Class Loading

実行時に名前でアクセスするようなクラスは、イメージのビルド時にあらかじめ列挙されている必要があります。例えば、Class.forName("myClass") を呼び出すには、設定ファイルに myClass が記述されていなければなりません。コンフィグレーションファイルにダイナミック・クラス・ローディングで要求されるクラスが含まれていない場合、ClassNotFoundException がスローされます。

Reflection

このカテゴリには、クラスのメソッドやフィールドを一覧したり、リフレクションによるメソッドの呼び出しやフィールドへのアクセス、java.lang.reflect パッケージから他のクラスを利用する、などが該当します。

リフレクションによってアクセスするそれぞれのクラス、メソッド、フィールドは既知である必要があります(ahead-of-time)。ネイティブイメージのビルドでは、静的解析によりリフレクションAPIの呼び出しを検出して、これらの要素を解決しようとします。解析に失敗した場合には、実行時にリフレクションでアクセスされるプログラム要素は、ネイティブイメージの生成時にコンフィグレーションファイルで指定するか、Feature から RuntimeReflection を使用して指定する必要があります。詳細はReflection support guideを参照してください。

ネイティブイメージの生成時には、リフレクションは制限無く使用できます。

Dynamic Proxy

このカテゴリには、java.lang.reflect.Proxy API を使用したダイナミックプロキシクラスの生成とインスタンス化が該当します。

ダイナミックプロキシクラスは、ahead-of-time でバイトコードが生成された場合に限り closed-world での最適化がサポートされます。つまり、ダイナミックプロキシを定義するインターフェースのリストは、イメージのビルド時に既知である必要があります。ネイティブイメージは、単純な静的解析により java.lang.reflect.Proxy.newProxyInstance(ClassLoader, Class<?>[], InvocationHandler)java.lang.reflect.Proxy.getProxyClass(ClassLoader, Class<?>[]) の呼び出しをインタセプトし、自動的にインターフェースのリストを決定しようとします。インターフェイスのリストは コンフィグレーションファイル で指定することができます。詳細については、Dynamic Proxies support guideを参照してください。

JNI (Java Native Interface)

ネイティブ・コードは、リフレクションを利用した場合と同様に、Javaオブジェクト、クラス、メソッド、フィールドに名前でアクセスできます。そのため、JNI により名前でアクセスする Javaアーティファクトは、ネイティブ・イメージの生成時に コンフィグレーションファイルで指定する必要があります。詳細については、JNI Implementation ガイドを参照してください。

ネイティブイメージでは、JNI よりもはるかにシンプルでオーバーヘッドの少ない独自のネイティブインターフェースを提供しています。このインターフェースは、JavaとCの間の呼び出しや、JavaコードからCデータ構造へのアクセスを可能にします。ただし、CコードからJavaデータ構造にアクセスすることはできません。詳細については、org.graalvm.nativeimage.c およびそのサブパッケージの JavaDocをご覧ください。

Serialization

Javaシリアライゼーションは、機能するためにクラスのメタデータ情報を必要とし、ネイティブイメージ生成時にコンフィグレーションファイルで指定する必要があります。しかし、Javaのシリアライゼーションは、セキュリティ脆弱性の根深い原因となっています。Java アーキテクトによると、これらの問題を回避するために、既存のシリアライゼーションの方式を置き換えることを予定しているとのことです。


Features Incompatible with Closed-World Optimization

いくつかのJavaの機能は、closed-world な最適化の対象となっておらず、使用した場合にはフォールバックイメージが生成されます。

invokedynamic Bytecode and Method Handles

closed-world の過程のもとでは、呼び出される全てのメソッドと、その呼び出し先は既知でなければなりません。invokedynamic とメソッドハンドルは、実行時にメソッドの呼び出しをもたらしたり、呼び出されるメソッドを変更したりすることができます。

javac で生成された invokedynamic のユースケース(例えば Javaのラムダ式や文字列連結など) は、イメージの実行時に呼び出されるメソッドを変更しないため、サポート対象となります。

Security Manager

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


Features That May Operate Differently in Native Image

ネイティブイメージは、Java HotSpot VMとは異なる方法で、いくつかのJava機能を実現しています。

Signal Handlers

シグナル・ハンドラを登録するには、シグナルを処理し、シャットダウン・フックを呼び出す新しいスレッドを起動する必要があります。デフォルトでは、ユーザーが明示的に登録しない限り、ネイティブ・イメージのビルド時にシグナル・ハンドラは登録されません。

例えば,共有ライブラリをビルドする際にデフォルトのシグナル・ハンドラを登録することは推奨されませんが、Dockerコンテナのようなコンテナ化された環境のためのネイティブイメージをビルドする際には、シグナルハンドラを含めることが望ましいと言えます。

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

Class Initializers

デフォルトでは、クラスはイメージの実行時に初期化されます。これにより互換性は確保されますが、一部の最適化が制限されます。

起動の高速化とピークパフォーマンスの向上のためには、イメージのビルド時にクラスを初期化することが望ましいことがあります。クラスの初期化の動作は、オプション --initialize-at-build-time--initialize-at-run-time を使って、特定のクラスやパッケージに対して、またはすべてのクラスに対して変更することができます。詳細については、native-image --help を参照してください。JDKのライブラリクラスについては、ユーザーが特別な配慮をする必要はありません。

ネイティブイメージのユーザーは、イメージのビルド時にクラスを初期化すると、既存のコードの特定の前提条件が崩れる可能性があることに注意する必要があります。例えば、クラスイニシャライザでロードされたファイルは、イメージビルド時とイメージランタイム時では同じ場所に無いかもしれません。さらに、ファイルディスクリプタや実行中のスレッドなどの特定のオブジェクトは、ネイティブイメージバイナリに格納してはいけません。もし、このようなオブジェクトがイメージのビルド時に到達可能な場合には、イメージの生成はエラーになります。

Finalizers

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

ファイナライザは実装が複雑で、不適切な設定となっているため、Java 9 から非推奨となっています。例えば、ファイナライザは、オブジェクトをスタティック・フィールドに格納することで、そのオブジェクトを再び到達可能にすることができます。そのため、ファイナライザーは呼び出されません。あらゆる Java VM で使用するために、ファイナライザを弱参照や参照キューに置き換えることが推奨されます。

Threads

ネイティブイメージで java.lang.ThreadThread.stop() などの長く非推奨となっているメソッドは実装されていません。

Unsafe Memory Access

sun.misc.Unsafe を使用してアクセスされるフィールドは、イメージのビルド時にクラスが初期化される場合、静的解析のためにそのようにマークする必要があります。多くの場合、これは自動的に行われます。static final フィールドに格納されたフィールドオフセットは、ホストされた値(イメージジェネレータが動作しているVMのフィールドオフセット)からネイティブイメージの値に自動的に書き換えられ、その書き換えの一部としてフィールドに Unsafe-accessed のマークが付けられます。標準的でないパターンの場合は、RecomputeFieldValue アノテーションを使用してフィールドオフセットを手動で再計算できます。

Debugging and Monitoring

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