トレース・エージェントを使用した構成支援

ネイティブ・イメージは実行時の前にビルドされ、そのビルドはアクセス可能なコードの静的分析に依存します。ただし、この分析では、Java Native Interface (JNI)、Javaリフレクション、動的プロキシ・オブジェクト(java.lang.reflect.Proxy)またはクラスパス・リソース(Class.getResource)のすべての使用状況を常に完全に予測できるわけではありません。これらの動的機能の検出されない使用状況は、構成ファイルの形式でnative-imageツールに渡す必要があります。

これらの構成ファイルの準備をより簡便にするために、GraalVMには、通常のJava VMでの実行のすべての動的機能の使用状況を追跡するエージェントが用意されています。これは、GraalVM javaコマンドのコマンドラインで有効にできます:

$JAVA_HOME/bin/java -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/ ...

-agentlibは、javaコマンドラインで-jarオプション、クラス名またはアプリケーション・パラメータのに指定する必要があります。

実行中、エージェントはJava VMとインタフェースして、クラス、メソッド、フィールド、リソースを参照したり、プロキシ・アクセスをリクエストするすべてのコールをインターセプトします。次に、エージェントは、指定された出力ディレクトリ(前述の例では/path/to/config-dir/)にファイルjni-config.jsonreflect-config.jsonproxy-config.jsonおよびresource-config.jsonを生成します。生成されるファイルは、インターセプトされたすべての動的アクセスを含むJSON形式のスタンドアロン構成ファイルです。

動的アクセスの範囲を改善するために、異なる入力を使用してターゲット・アプリケーションを複数回実行し、個別の実行パスをトリガーする必要がある場合があります。これに対応するには、エージェントに対してconfig-merge-dirオプションを使用し、インターセプトされたアクセスが既存の構成ファイル・セットに追加されるようにします:

$JAVA_HOME/bin/java -agentlib:native-image-agent=config-merge-dir=/path/to/config-dir/ ...
                                                              ^^^^^

config-merge-dirの使用時に、指定されたターゲット・ディレクトリまたは構成ファイルがない場合、エージェントはそれらを作成して警告を出力します。

デフォルトでは、エージェントはJVMプロセスの終了後に構成ファイルを書き込みます。さらに、エージェントには、構成ファイルを定期的に書き込むための次のフラグが用意されています:

たとえば:

$JAVA_HOME/bin/java -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/,config-write-period-secs=300,config-write-initial-delay-secs=5 ...

生成された構成ファイルを手動で確認することをお薦めします。これは、実行されたコードのみがエージェントによって監視され、生成された構成では、他のコード・パスで使用されている要素が欠落する可能性があるためです。また、生成された構成を簡素化して、将来の手動メンテナンスを容易にすることもできます。

生成された構成ファイルは、たとえばイメージ・ビルドで使用されるJARファイル内のクラスパスのMETA-INF/native-image/ディレクトリに配置することで、native-imageツールに渡すことができます。このディレクトリ(またはそのサブディレクトリ)では、jni-config.jsonreflect-config.jsonproxy-config.jsonおよびresource-config.jsonという名前のファイルが検索され、自動的にビルドに組み込まれます。これらのファイルがすべて存在する必要はありません。同じ名前のファイルが複数見つかった場合は、それらがすべて組み込まれます。

Javaリフレクションを使用したネイティブ・イメージのビルド例

デモ用に、次のコードをReflectionExample.javaファイルとして保存します:

import java.lang.reflect.Method;

class StringReverser {
    static String reverse(String input) {
        return new StringBuilder(input).reverse().toString();
    }
}

class StringCapitalizer {
    static String capitalize(String input) {
        return input.toUpperCase();
    }
}

public class ReflectionExample {
    public static void main(String[] args) throws ReflectiveOperationException {
        String className = args[0];
        String methodName = args[1];
        String input = args[2];

        Class<?> clazz = Class.forName(className);
        Method method = clazz.getDeclaredMethod(methodName, String.class);
        Object result = method.invoke(null, input);
        System.out.println(result);
    }
}

これは、プログラム要素に名前でアクセスするための非定数文字列が外部入力として必要な単純なJavaプログラムです。メイン・メソッドでは、コマンドライン引数として名前が渡される特定のクラス(Class.forName)のメソッドを起動します。コマンドラインで他のクラス名またはメソッド名を指定すると、例外が発生します。

例をコンパイルしたら、各メソッドを起動します:

$JAVA_HOME/bin/javac ReflectionExample.java
$JAVA_HOME/bin/java ReflectionExample StringReverser reverse "hello"
olleh
$JAVA_HOME/bin/java ReflectionExample StringCapitalizer capitalize "hello"
HELLO

リフレクション構成ファイルを使用せずに通常どおりネイティブ・イメージをビルドし、結果のイメージを実行します:

$JAVA_HOME/bin/native-image ReflectionExample
[reflectionexample:59625]    classlist:     467.66 ms
...
./reflectionexample

reflectionexampleバイナリは、JVM用のランチャにすぎません。リフレクティブなルックアップ操作を使用してネイティブ・イメージをビルドするには、トレース・エージェントを適用して、後でネイティブ・イメージ・ビルドに読み込まれる構成ファイルを書き込みます。

  1. 作業ディレクトリにMETA-INF/native-imageディレクトリを作成します:
    mkdir -p META-INF/native-image
    
  2. エージェントを有効にし、必要なコマンドライン引数を渡します:
    $JAVA_HOME/bin/java -agentlib:native-image-agent=config-output-dir=META-INF/native-image ReflectionExample StringReverser reverse "hello"
    

    このコマンドでは、リフレクションを介してStringReverserクラスおよびreverse()メソッドにアクセスできるようにするreflection-config.jsonファイルを作成します。jni-config.jsonproxy-config.jsonおよびresource-config.json構成ファイルもこのディレクトリに書き込まれます。

  3. ネイティブ・イメージをビルドします:
    $JAVA_HOME/bin/native-image --no-fallback ReflectionExample
    

    ネイティブ・イメージ・ビルダーでは、META-INF/native-imageディレクトリまたはサブディレクトリ内の構成ファイルが自動的に取得されます。ただし、JARファイルまたは-cpフラグを使用して、クラスパスにMETA-INF/native-imageの場所を指定することをお薦めします。これは、ツールによってディレクトリ構造が定義されるIDEユーザーの混乱を避けるために役立ちます。

  4. メソッドをテストしますが、両方をサポートする構成を作成するためにトレース・エージェントを2回実行していないことに留意してください:
    ./reflectionexample StringReverser reverse "hello"
    olleh
    ./reflectionexample  StringCapitalizer capitalize "hello"
    Exception in thread "main" java.lang.ClassNotFoundException: StringCapitalizer
     at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:60)
     at java.lang.Class.forName(DynamicHub.java:1161)
     at ReflectionExample.main(ReflectionExample.java:21)
    

トレース・エージェントもネイティブ・イメージ・ジェネレータも、指定された構成ファイルが完全かどうかを自動的にチェックすることはできません。エージェントは、ネイティブ・イメージで同じアクセスが可能になるように、リフレクションを介してアクセスされる値のみを監視および記録します。reflection-config.jsonファイルを手動で編集するか、トレース・エージェントを再実行して既存の構成ファイルを変換するか、config-merge-dirオプションを使用して拡張できます:

$JAVA_HOME/bin/java -agentlib:native-image-agent=config-merge-dir=META-INF/native-image ReflectionExample StringCapitalizer capitalize "hello"

異なるconfig-merge-dirオプションでは、既存の構成ファイルを上書きせずに拡張するようにエージェントに指示します。ネイティブ・イメージを再ビルドすると、StringCapitalizerクラスおよびcapitalizeメソッドにもアクセスできるようになります。

エージェントの高度な使用方法

コール元ベースのフィルタ

デフォルトでは、エージェントは、ネイティブ・イメージが構成なしでサポートする動的アクセスをフィルタ処理します。フィルタ・メカニズムでは、アクセスを実行するJavaメソッド(コール元メソッドとも呼ばれる)が識別され、その宣言クラスが一連のフィルタ・ルールと照合されます。組込みフィルタ・ルールでは、生成された構成ファイルから、JVMからの動的アクセス、またはネイティブ・イメージで直接サポートされているJavaクラス・ライブラリの一部(java.nioなど)からの動的アクセスが除外されます。アクセス対象の項目の種類(クラス、メソッド、フィールド、リソースなど)はフィルタ処理には関係ありません。

組込みフィルタに加えて、caller-filter-fileオプションを使用して、追加のルールを含むカスタム・フィルタ・ファイルを指定できます。たとえば、-agentlib:caller-filter-file=/path/to/filter-file,config-output-dir=...のように指定します

フィルタ・ファイルの構造は次のとおりです:

{ "rules": [
    {"excludeClasses": "com.oracle.svm.**"},
    {"includeClasses": "com.oracle.svm.tutorial.*"},
    {"excludeClasses": "com.oracle.svm.tutorial.HostedHelper"}
  ]
}

rulesセクションには、一連のルールを指定します。各ルールでは、includeClasses (一致するクラスで発生したルックアップが結果の構成に追加される)またはexcludeClasses (一致するクラスで発生したルックアップが構成から除外される)を指定します。各ルールでは、末尾を.*または.**として、照合する一連のクラスのパターンを定義します。末尾を.*とすると、該当するパッケージ内のみのすべてのクラスと照合することに対し、末尾を.**とすると、該当するパッケージ内のすべてのクラスだけでなく、任意の深さのすべてのサブパッケージ内のすべてのクラスと照合します。.*または.**を指定しない場合、ルールはパターンと一致する修飾名を持つ単一のクラスにのみ適用されます。すべてのルールは指定された順序で処理されるため、後のルールによって前のルールの一部または全部をオーバーライドできます。複数のフィルタ・ファイルが指定されている(複数のcaller-filter-fileオプションを指定することによって)場合、それらのルールはファイルが指定されている順序で連鎖されます。組込みのコール元フィルタのルールは常に最初に処理されるため、カスタム・フィルタ・ファイルでオーバーライドできます。

前述の例では、最初のルールにより、生成された構成から、パッケージcom.oracle.svmおよびそのすべてのサブパッケージ(さらにそれらのサブパッケージ)に含まれているすべてのクラスで発生したルックアップが除外されます。ただし、次のルールでは、パッケージcom.oracle.svm.tutorialに直接含まれているクラスからのルックアップが再度追加されます。最後に、HostedHelperクラスからのルックアップが再度除外されます。これらの各ルールは、前のルールを部分的にオーバーライドします。たとえば、ルールが逆順であった場合、com.oracle.svm.**の除外が最後のルールとなり、他のすべてのルールがオーバーライドされます。

テスト目的で、no-builtin-caller-filterオプションを追加することでJavaクラス・ライブラリのルックアップの組込みフィルタを無効にできますが、結果の構成ファイルは一般的にネイティブ・イメージ・ビルドには適しません。同様に、ヒューリスティックに基づくJava VM内部アクセス用の組込みフィルタは、no-builtin-heuristic-filterを使用して無効にできますが、そのようにすると、一般的には構成ファイルの有用性が低下します。たとえば、-agentlib:native-image-agent=no-builtin-caller-filter,no-builtin-heuristic-filter,config-output-dir=...のように指定します

アクセス・フィルタ

発生場所に基づいて動的アクセスをフィルタ処理する前述のコール元ベースのフィルタとは異なり、アクセス・フィルタはアクセスのターゲットに適用されます。したがって、アクセス・フィルタを使用すると、生成された構成からパッケージとクラス(およびそのメンバー)を直接除外できます。

デフォルトでは、アクセスされるすべてのクラス(コール元ベースのフィルタおよび組込みフィルタも通過するもの)が生成された構成に追加されます。access-filter-fileオプションを使用すると、前述のファイル構造に従ったカスタム・フィルタ・ファイルを追加できます。このオプションを複数回指定して複数のフィルタ・ファイルを追加し、他のフィルタ・オプションと組み合せることができます。たとえば、-agentlib:access-filter-file=/path/to/access-filter-file,caller-filter-file=/path/to/caller-filter-file,config-output-dir=...のように指定します

ネイティブ・イメージ引数としての構成ファイルの指定

構成ファイルを含むディレクトリがクラスパス上にない場合は、native-imageに対して-H:ConfigurationFileDirectories=/path/to/config-dir/を使用して指定できます。このディレクトリには、jni-config.jsonreflect-config.jsonproxy-config.jsonおよびresource-config.jsonの4つのファイルをすべて直接含める必要があります。4つの同じ構成ファイルを含むディレクトリがクラスパス上にあるものの、META-INF/native-image/内にない場合は、-H:ConfigurationResourceRoots=path/to/resources/を使用して指定できます。-H:ConfigurationFileDirectories-H:ConfigurationResourceRootsのいずれでも、カンマで区切ったディレクトリのリストを指定できます。

プロセス環境を介したエージェントの注入

javaコマンドラインを変更してエージェントを注入することは、Javaプロセスがアプリケーションまたはスクリプト・ファイルによって起動される場合、またはJavaが既存のプロセスに埋め込まれている場合は、明らかに困難です。その場合、JAVA_TOOL_OPTIONS環境変数を使用してエージェントを注入することもできます。この環境変数は、同時に実行される複数のJavaプロセスで取得できます。その場合、各エージェントはconfig-output-dirを使用して個別の出力ディレクトリに書き込む必要があります。(次の項では、構成ファイル・セットをマージする方法について説明します。)単一のグローバルJAVA_TOOL_OPTIONS変数で個別のパスを使用するために、エージェントの出力パス・オプションではプレースホルダがサポートされています:

export JAVA_TOOL_OPTIONS="-agentlib:native-image-agent=config-output-dir=/path/to/config-output-dir-{pid}-{datetime}/"

{pid}プレースホルダはプロセス識別子に置き換えられ、{datetime}はUTCでのシステム日時に置き換えられて、ISO 8601に従ってフォーマットされます。前述の例では、結果のパスは/path/to/config-output-dir-31415-20181231T235950Z/のようになります。

トレース・ファイル

前述の例では、native-image-agentを使用して、Java VMでの動的アクセスを追跡し、そこから構成ファイル・セットを生成しています。ただし、実行をより深く理解するために、エージェントは個々のアクセスを含むトレース・ファイルをJSON形式で書き込むこともできます:

$JAVA_HOME/bin/java -agentlib:native-image-agent=trace-output=/path/to/trace-file.json ...

native-image-configureツールでは、トレース・ファイルをネイティブ・イメージ・ビルドで使用可能な構成ファイルに変換できます。次のコマンドでは、trace-file.jsonを読み取って処理し、ディレクトリ/path/to/config-dir/に構成ファイル・セットを生成します:

native-image-configure generate --trace-input=/path/to/trace-file.json --output-dir=/path/to/config-dir/

相互運用性

エージェントはGraalVMとともに配布されますが、JVM Tool Interface (JVMTI)を使用します。また、JVMTIをサポートする他のJVMとともに使用することもできます。この場合、エージェントの絶対パスを指定する必要があります:

/path/to/some/java -agentpath:/path/to/graalvm/jre/lib/amd64/libnative-image-agent.so=<options> ...

ネイティブ・イメージ構成ツール

前の項で説明したように複数のプロセスでエージェントを同時に使用する場合、config-output-dirは安全なオプションですが、結果として複数の構成ファイル・セットが生成されます。native-image-configure-launcherツールを使用すると、これらの構成ファイルをマージできます。このツールは、次のように事前にビルドする必要があります:

native-image --macro:native-image-configure-launcher

ノート: ネイティブ・イメージ構成ツールは、native-imagemxを介してビルドされている場合にのみ使用できます。この構成ツールは、デフォルトではGraalVMディストリビューションに含まれていません。

その後、このツールを使用して、次のように構成ファイル・セットをマージできます:

native-image-configure-launcher generate --input-dir=/path/to/config-dir-0/ --input-dir=/path/to/config-dir-1/ --output-dir=/path/to/merged-config-dir/

このコマンドでは、/path/to/config-dir-0//path/to/config-dir-1/からそれぞれ異なる構成ファイル・セットを読み取り、両方の情報を含む構成ファイル・セットを/path/to/merged-config-dir/に書き込みます。

構成ファイル・セットを使用して、任意の数の--input-dir引数を指定できます。すべてのオプションについては、native-image-configure-launcher helpを参照してください。