Introducing the Tracing Agent: Simplifying GraalVM Native Image Configuration

このエントリは以下のエントリをベースにしています。
This entry is based on the following, written by Christian Wimmer [VM and compiler researcher at Oracle Labs, and Project lead for GraalVM native image generation (Substrate VM)]
https://medium.com/graalvm/introducing-the-tracing-agent-simplifying-graalvm-native-image-configuration-c3b56c486271

tl;dr:

トレースエージェントは、GraalVMやその他の互換VM上で動作するJavaアプリケーションの挙動を記録し、リフレクションやJNI、リソース、プロキシ利用のための設定ファイルをGraalVM Native Image Generatorに提供する。
以下のコマンドで有効化する。

java -agentlib:native-image-agent=…

Introduction

GraalVMでのネイティブイメージ生成では、静的解析と事前コンパイルを適用して、Javaアプリケーション用にネイティブイメージと呼ばれる最適化されたネイティブ実行可能ファイルを構築します。これには到達可能なアプリケーションクラスの閉世界仮説(closed-world assumption)が必要です。つまり、静的解析で処理できるように、すべてのクラスはネイティブイメージ生成時にわかっている必要があります。

GraalVM Native Image
https://www.graalvm.org/docs/reference-manual/aot-compilation/

閉世界仮説はJavaのリフレクションのopen-worldアプローチと矛盾します。java.lang.reflect パッケージの機能を使い、アプリケーション開発者はクラスやメソッド、フィールドを名前で検索し、それらにアクセスしたり呼び出したりします。通常は、名前を設定ファイルから呼び出したり、実行時に動的に設定したりします。ネイティブイメージ生成にあたってもリフレクションはサポートするものの、ネイティブイメージ生成時にリフレクション対象のすべての要素をリスト化しておく必要があります。必要なJSONファイルの構造については以下のドキュメントで説明されています。

Reflection on Substrate VM
https://github.com/oracle/graal/blob/master/substratevm/REFLECTION.md

このエントリでは、Java HotSpot VM上での実行時、つまり、ネイティブ・イメージとしてではない状態でアプリケーションを実行しているときのアプリケーションの動作を観察することによって、JSONファイルを生成する新しいトレース・エージェントをご紹介します。 これは、アプリケーションの開発とテストはJava HotSpot VMを使用して行い、その後デプロイ前に最終的なアプリケーションのみをネイティブイメージに変換するという、共通のワークフローを活用します。つまり、 開発およびテスト中にJava HotSpot VMをトレースすることによって、必要なリフレクション設定ファイルを作成します。

トレースエージェントは、GraalVM Community EditionおよびGraalVM Enterprise Editionの両方に含まれています。有効化するには、以下のオプションを使います。エージェントのコマンドは以下の例で紹介します。

 -agentlib:native-image-agent=... 

Example

例としてリフレクションを使う最小限の”Hello, world!”アプリケーションを使います。このエントリでは環境変数JAVA_HOMEはGraalVM(19.0以後)のインストール先が指定されており、native-imageツールがインストール済みとします。

GraalVM Native Image – prerequisites
https://www.graalvm.org/docs/reference-manual/aot-compilation/#install-native-image

以下がReflection APIを使ったサンプルJavaアプリケーションです。

mainメソッドでコマンドライン引数として渡された名前の全メソッドを呼び出します。簡単のために2個のメソッド(fooとbar)だけにしておきます。コマンドラインでその他の名前を渡した場合には例外が発生します。

以下のようにサンプルを実行すると

$JAVA_HOME/bin/java HelloReflection foo xyz

以下のような結果が帰ってきます。

Running foo
Exception running xyz: NoSuchMethodException

想定通り、メソッドfooがリフレクションの結果判明しましたが、存在しないメソッドであるxyzは見つかりませんでした。

前述の通り、ネイティブイメージの生成にはリフレクション設定ファイルが必要です。リフレクション設定ファイルがない場合、リフレクションを使ってメソッドfooにアクセスできません。混乱を避けるため、リフレクション設定ファイルがないのにリフレクションが使われていることがわかった場合には、 ネイティブイメージ生成ツールが検知します。例えば以下を実行した場合、

$JAVA_HOME/bin/native-image HelloReflection

アプリケーションのネイティブイメージではなく、いわゆるフォールバックイメージだけが生成されます。

…
 Warning: Reflection method java.lang.Class.getMethod invoked at HelloReflection.main(HelloReflection.java:14)
 Warning: Abort stand-alone image build due to reflection use without configuration.
 Warning: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
 …
 Warning: Image 'helloreflection' is a fallback-image that requires a JDK for execution (use --no-fallback to suppress fallback image generation).

フォールバックイメージは単なるJava HotSpot VMのランチャーです。これはおそらく開発者が本当に欲しているものではありませんが、実行時にすぐに失敗するネイティブイメージを生成しないことを確実にすることが必要で、これは想定される動作です。

./helloreflection foo xyz
 Running foo
 Exception running xyz: NoSuchMethodException

–no-fallback というオプションをつけると、明示的にフォールバックイメージの生成を無効にできます。

$JAVA_HOME/bin/native-image --no-fallback HelloReflection

この場合、Java HotSpot VMがなくても動作するネイティブイメージを生成しますが、リフレクションを使ったメソッドへのアクセスはできません。

./helloreflection foo xyz
 Exception running foo: NoSuchMethodException
 Exception running xyz: NoSuchMethodException

Reflection Tracing Agent

完全なリフレクション設定ファイルを一から書き出すこともできますが、面倒です。そのため、Java HotSpot VMに対するすべてのリフレクション検索操作を追跡してリフレクション設定ファイルを生成する、Java HotSpot VM用のエージェントを提供します。トレースされる操作には、Class.forName、Class.getMethod、Class.getFieldOperationsなどがあります。エージェントはGraalVMのダウンロードバイナリに含まれています。

mkdir -p META-INF/native-image
$JAVA_HOME/bin/java -agentlib:native-image-agent=config-output-dir=META-INF/native-image HelloReflection foo xyz

このコマンドを実行すると、reflection-config.jsonファイルを含むMETA-INF/native-imageというディレクトリを作成します。その他いくつかのファイルをこのディレクトリに作成しますが、これらについては後述します。reflection-config.jsonファイルがあると、リフレクションによりHelloReflection.fooメソッドにアクセスできるようになります。

[
  {
    "name":"HelloReflection",
    "methods":[{ "name":"foo", "parameterTypes":[] }]
  }
]

ネイティブイメージ生成ツールは自動的に設定ファイルをMETA-INF/native-imageもしくはそのサブディレクトリから見つけ出します。これはnative-image.propertiesファイルを自動検出するのと同じお作法です。

Simplifying native-image generation with Maven plugin and embeddable configuration
https://medium.com/graalvm/simplifying-native-image-generation-with-maven-plugin-and-embeddable-configuration-d5b283b92f57
https://logico-jp.io/2019/03/25/simplifying-native-image-generation-with-maven-plugin-and-embeddable-configuration/

$JAVA_HOME/bin/native-image HelloReflection

コマンドを実行すると、リフレクションでメソッドfooを検索できるネイティブイメージを生成します。注意いただきたいのは、–no-fallbackオプションを渡す必要がない、ということです。リフレクション設定ファイルは、アプリケーションがリフレクションを使用するという事実にもかかわらず、フォールバックイメージを生成してはならないという開発者の意図を伝えています。このネイティブイメージは期待どおりに動作します。

./helloreflection foo xyz
Running foo
Exception running xyz: NoSuchMethodException

そして期待通りにネイティブイメージは瞬時に起動します。

サンプルアプリケーションのGraalVMネイティブイメージは瞬時に起動します。

Completeness of Reflection Configuration

トレースエージェントとネイティブイメージツールは、トレースされたリフレクションの使用法、または提供されたリフレクション設定ファイルが完全であることを自動的に確認できません。 ここまでサンプルを実行する際に、コマンドラインでメソッドbarの名前を指定していません。 このメソッドは、Java HotSpot VMでサンプルを実行したときに見つかります。

$JAVA_HOME/bin/java HelloReflection bar
Running bar

しかし前章で生成したネイティブイメージを実行すると、メソッドbarが見つかりません。

./helloreflection bar
Exception running bar: NoSuchMethodException

対処するためには、reflection-config.jsonを手作業で修正してメソッドbarを追加するか、トレースエージェントを実行して設定ファイルを増やす必要があります。

$JAVA_HOME/bin/java -agentlib:native-image-agent=config-merge-dir=META-INF/native-image HelloReflection bar

新しい設定ファイルで上書きするのではなく、既存の設定ファイルを拡張するようにエージェントに指示するconfig-merge-dirという別のオプションがあります。ネイティブイメージを再ビルドすると、メソッドbarにもアクセスできるようになりました。

$JAVA_HOME/bin/native-image HelloReflection
 …
 ./helloreflection foo bar xyz
 Running foo
 Running bar
 Exception running xyz: NoSuchMethodException

実際のアプリケーションでは、トレースエージェントと手作業による検査と設定ファイルの変更を組み合わせることを推奨します。アプリケーションが提供するすべてのテストスイートでJava HotSpot VM上で実行すると、かなり完成度の高い設定ファイルを作成できます。完全性はテストスイートのコードカバレッジによって異なります。100%のアプリケーションコードカバレッジを持つ理想的なテストスイートは、完全性が保証された設定ファイルを生成します。ただし、実際にはテストスイートはアプリケーションのすべてのパスを通すテストを実行することはありません。そのため、実際のアプリケーションでは手動による設定ファイルの検査と変更が必要になる可能性があります。

JNI, Resource, and Proxy Configuration

ネイティブイメージ生成ツールはリフレクションのためだけでなく、静的解析でネイティブイメージに何を入れるかを自動的に決定できない他のいくつかの機能のためにも設定ファイルが必要です。

  • JNI
    Java Native Interface (JNI) on Substrate VM
    https://github.com/oracle/graal/blob/master/substratevm/JNI.md
    JNIを使ってCのコードからアクセスされるクラス、メソッド、フィールドを、リフレクション設定ファイルと同じ構造を持つファイルを使い登録しなければなりません。
  • Resources
    Accessing resources in Substrate VM images
    https://github.com/oracle/graal/blob/master/substratevm/RESOURCES.md
    アプリケーションデータファイルはクラスと並んでJARファイルに含まれていることが多々あります。上記URLに記載の通り、実行時に利用可能なすべてのリソースは、正規表現の構文を使用して指定されなければなりません。
  • Proxy
    Dynamic proxies on Substrate VM
    https://github.com/oracle/graal/blob/master/substratevm/DYNAMIC_PROXY.md
    java.lang.reflect.Proxyの内部実装では、java.lang.reflect.Proxyに渡されたインターフェースのすべての組み合わせに対応するクラスを生成します。これらの組み合わせtは上記URLに記載の通り、構成ファイルで提供される必要があります。

トレースエージェントは、アプリケーションによるJNI、リソース、Proxyの利用状況も追跡し、適切な設定ファイル(jni-config.json、resource-config.json、proxy-config.json)を生成します。

Conclusions

トレースエージェントは、Java HotSpot VM上で実行されているアプリケーションの動作を監視し、自動的にネイティブイメージ生成ツールを構成するための設定ファイルに書き込みます。このトレースエージェントが、アプリケーションを初めてネイティブイメージとして実行し、継続的インテグレーションビルド/テストシステムの一部として実行するのに役立つツールであることを願っています。このエージェントは最近追加されたものなので、バグに遭遇した場合や特定の機能が欠けている場合はお知らせください。

トレースエージェントを利用するには、まずGraalVMをダウンロードしてください。

GraalVM Downloads
https://www.graalvm.org/downloads/

その後、guユーティリティでnative-imageコンポーネントをインストールする必要があります。

gu install native-image

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中