デモ・アプリケーションの実行

Java on TruffleはJava仮想マシン仕様の実装となり、Javaまたは他のJVM言語でアプリケーションを実行できる以外に、いくつかの興味深い機能を提供します。Java on Truffleでできることを説明するために、次の簡単な例について考えてみます。

Java on Truffleでの拡張ホットスワップ機能

Java on Truffleで実行されるアプリケーションに対して、組込みの拡張ホットスワップ機能を使用できます。拡張ホットスワップの利点を得るために、アプリケーションをデバッグ・モードで起動して標準のIDEデバッガをアタッチする以外に特定の構成を行う必要はありません。

Java on Truffleでのデバッグ

任意のIDEデバッガを使用して、Java on Truffleランタイムで実行されているJavaアプリケーションをデバッグできます。たとえば、IntelliJ IDEAからのデバッガ・セッションの開始は実行構成に基づきます。デバッガを同じ環境内のJavaアプリケーションにアタッチするには、メイン・メニューで「Run」→「Debug…」→「Edit Configurations」に移動し、「Environment」を展開して、JREの値とVMオプションの値を確認します。GraalVMがプロジェクトのJREとして表示され、VMオプションに-truffle -XX:+IgnoreUnrecognizedVMOptionsが含まれている必要があります。まだサポートされていない-javaagent引数がIntellijによって自動的に追加されるため、-XX:+IgnoreUnrecognizedVMOptionsを指定する必要があります。「Debug」を押します。

これにより、アプリケーションが実行され、デバッガ・セッションがバックグラウンドで開始されます。

デバッグ・セッションでのコード変更の適用

デバッガ・セッションを実行すると、セッションを再起動する必要なく、幅広いコード変更(ホットスワップ)を適用できます。独自のアプリケーションで、または次の手順に従って、これを自由に試してください:

  1. 新しいJavaアプリケーションを作成します。
  2. 次のmainメソッドを開始点として使用します:

           public class HotSwapDemo {
    
               private static final int ITERATIONS = 100;
    
               public static void main(String[] args) {
                   HotSwapDemo demo = new HotSwapDemo();
                   System.out.println("Starting HotSwap demo with Java on Truffle: 'java.vm.name' = " + System.getProperty("java.vm.name"));
                   // run something in a loop
                   for (int i = 1; i <= ITERATIONS; i++) {
                       demo.runDemo(i);
                   }
                   System.out.println("Completed HotSwap demo with Java on Truffle");
               }
    
               public void runDemo(int iteration) {
                   int random = new Random().nextInt(iteration);
                   System.out.printf("\titeration %d ran with result: %d\n", iteration, random);
               }
           }
    
  3. Espressoで実行していることをjava.vm.nameプロパティが示していることを確認します。
  4. runDemo()の最初の行に行ブレークポイントを設定します。
  5. Java on Truffleで実行するように実行構成を設定し、「Debug」を押します。次のように表示されます:

    debug-1

  6. ブレークポイントで一時停止している間に、runDemo()の本体からメソッドを抽出します:

    debug-2

  7. 「Run」→「Debugging Actions」→「Reload Changed Classes」に移動して、変更をリロードします:

    debug-3

  8. 「Debug」→「Frames view」で<obsolete>:-1の現在のフレームを確認して、変更が適用されたことを検証します:

    debug-4

  9. 新しく抽出したメソッドの最初の行にブレークポイントを設定し、「Resume Program」を押します。ブレークポイントにヒットします:

    debug-5

  10. printRandom()のアクセス修飾子をprivateからpublic staticに変更してみます。変更をリロードします。「Resume Program」を押して変更が適用されたことを検証します:

    debug-6

Java on Truffleでの拡張ホットスワップ機能のデモのビデオ・バージョンを視聴してください。


サポートされる変更

Java on Truffleでアプリケーションを実行するときの任意のコード変更をサポートする計画です。GraalVM 21.1.0の時点で、次の変更がサポートされています:

  1. メソッドの追加および削除
  2. コンストラクタの追加および削除
  3. インタフェースに対するメソッドの追加および削除
  4. メソッドのアクセス修飾子の変更
  5. コンストラクタのアクセス修飾子の変更
  6. ラムダに対する変更
  7. 新しい匿名内部クラスの追加
  8. 匿名内部クラスの削除

GraalVM 21.1.0の時点で、次の制限が残っています:

  1. フィールドに対する変更
  2. クラス・アクセス修飾子に対する変更(たとえば、抽象から具象へ)
  3. スーパークラスの変更
  4. 実装されたインタフェースの変更
  5. 列挙に対する変更

JavaでのAOTとJITの混合

GraalVMネイティブ・イメージテクノロジを使用すると、アプリケーションを次のような実行可能なネイティブ・バイナリにAhead-of-Time (AOT)コンパイルできます:

ネイティブ・イメージを使用する場合の主なトレードオフは、プログラムの分析およびコンパイルが閉世界仮説の下で発生すること、つまり、アプリケーションで実行されるすべてのバイトコードを静的分析で処理する必要があることです。これにより、動的なクラス・ロードやリフレクションなどの一部の言語機能の使用が複雑になります。

Java on Truffleは、Truffleフレームワーク上にビルドされた、JVMバイトコード・インタプリタのJVM実装です。これは、Truffleフレームワーク自体およびGraalVM JITコンパイラと同様に、基本的にはJavaアプリケーションです。これら3つはすべて、native-imageを使用して事前にコンパイルできます。アプリケーションの一部に対してJava on Truffleを使用すると、必要な動的動作を分離し、コードの残りの部分で引き続きネイティブ実行可能ファイルを使用できます。

コマンドライン・アプリケーションの例として、正規のJava Shellツール(JShell)について考えます。これはJavaコードを評価できるREPLで、2つの部分で構成されています:

この設計は、例で説明しようとしている点にそのまま適合しています。JShellのUI部分のネイティブ実行可能ファイルをビルドし、それにJava on Truffleを含めて、実行時に動的に指定されたコードを実行できます。

前提条件:

  1. デモ・アプリケーションを含むプロジェクトをクローニングし、espresso-jshellディレクトリに移動します:
git clone https://github.com/graalvm/graalvm-demos.git
cd graalvm-demos/espresso-jshell

JShell実装は実際には通常のJShellランチャ・コードであり、これは実行エンジンのJava on Truffle実装(プロジェクト・コード名はEspresso)のみを受け入れます。

AOTコンパイル済の部分とコードを動的に評価するコンポーネントをバインドするグルー・コードは、EspressoExecutionControlクラスにあります。これは、Java on Truffleコンテキスト内にJShellクラスをロードし、それらに入力を委任します:

    protected final Lazy<Value> ClassBytecodes = Lazy.of(() -> loadClass("jdk.jshell.spi.ExecutionControl$ClassBytecodes"));
    protected final Lazy<Value> byte_array = Lazy.of(() -> loadClass("[B"));
    protected final Lazy<Value> ExecutionControlException = Lazy.of(() -> loadClass("jdk.jshell.spi.ExecutionControl$ExecutionControlException"));
    protected final Lazy<Value> RunException = Lazy.of(() -> loadClass("jdk.jshell.spi.ExecutionControl$RunException"));
    protected final Lazy<Value> ClassInstallException = Lazy.of(() -> loadClass("jdk.jshell.spi.ExecutionControl$ClassInstallException"));
    protected final Lazy<Value> NotImplementedException = Lazy.of(() -> loadClass("jdk.jshell.spi.ExecutionControl$NotImplementedException"));
    protected final Lazy<Value> EngineTerminationException = Lazy.of(() -> loadClass("jdk.jshell.spi.ExecutionControl$EngineTerminationException"));
    protected final Lazy<Value> InternalException = Lazy.of(() -> loadClass("jdk.jshell.spi.ExecutionControl$InternalException"));
    protected final Lazy<Value> ResolutionException = Lazy.of(() -> loadClass("jdk.jshell.spi.ExecutionControl$ResolutionException"));
    protected final Lazy<Value> StoppedException = Lazy.of(() -> loadClass("jdk.jshell.spi.ExecutionControl$StoppedException"));
    protected final Lazy<Value> UserException = Lazy.of(() -> loadClass("jdk.jshell.spi.ExecutionControl$UserException"));

値を正しく渡して例外を変換するためのコードがさらにあります。これを試すには、提供されているスクリプト(次のことを実行します)を使用してespresso-jshellバイナリをビルドします:

  1. Javaソースをバイトコードにビルドします
  2. JARファイルを構築します
  3. ネイティブ実行可能ファイルをビルドします

native-imageコマンドの最も重要な構成行は、Java on Truffle実装をバイナリに含めるように指定する--language:javaです。ビルド後に、結果のバイナリ・ファイルを確認できます(fileおよびlddはLinuxコマンドです)

file ./espresso-jshell
ldd ./espresso-jshell

これは実際にJVMに依存しないバイナリ・ファイルであり、実行して起動の速さを確認できます:

./espresso-jshell
|  Welcome to JShell -- Version 11.0.10
|  For an introduction type: /help intro

jshell> 1 + 1
1 ==> 2

新しいコードをJShellにロードしてみて、Java on Truffleによってどのように実行されるかを確認します。

Java on TruffleでのAOTとJITの混合コンパイル済コードのデモのビデオ・バージョンを視聴してください。


Java on TruffleでのGraalVMツール

Java on TruffleはGraalVMエコシステムに正式に含まれており、GraalVMでサポートされている他の言語と同様に、開発者ツールもデフォルトでサポートされています。Truffleフレームワークは、デバッガ、プロファイラ、メモリー・アナライザ、インストゥルメンテーションAPIなどのツールと統合されています。言語のインタプリタは、これらのツールをサポートするためにASTノードを注釈でマークする必要があります。

たとえば、プロファイラを使用できるように、言語インタプリタはルート・ノードをマークする必要があります。デバッガ用には、言語式をインストゥルメンタル、指定された変数のスコープなどとしてマークすることをお薦めします。言語のインタプリタは、ツール自体と統合する必要はありません。そのため、CPUサンプラまたはメモリー・トレーサ・ツールを使用して、Java on Truffleプログラムをそのままプロファイリングできます。

たとえば、素数を計算する次のようなクラスがあるとします:

import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.LongStream;

public class Main {

    public static void main(String[] args) {
        Main m = new Main();

        for (int i = 0; i < 100_000; i++) {
            System.out.println(m.random(100));
        }
    }

    private Random r = new Random(41);
    public List<Long> random(int upperbound) {
        int to = 2 + r.nextInt(upperbound - 2);
        int from = 1 + r.nextInt(to - 1);
        return primeSequence(from, to);
    }
    public static List<Long> primeSequence(long min, long max) {
        return LongStream.range(min, max)
                .filter(Main::isPrime)
                .boxed()
                .collect(Collectors.toList());
    }
    public static boolean isPrime(long n) {
        return LongStream.rangeClosed(2, (long) Math.sqrt(n))
                .allMatch(i -> n % i != 0);
    }
}

このプログラムをビルドし、--cpusamplerオプションを指定して実行します。

javac Main.java
java -truffle --cpusampler Main > output.txt

output.txtファイルの最後に、プロファイラ出力、メソッドのヒストグラムおよび実行にかかった時間が表示されます。このプログラム内の割当てが発生している箇所を確認するために、--memtracerオプションを試すこともできます。

java -truffle --experimental-options --memtracer Main > output.txt

GraalVMが提供するその他のツールには、Chromeデバッガコード・カバレッジおよびGraalVM Insightがあります。

開発者ツールが初期状態でサポートされるため、Java on TruffleはJVMの有力な選択肢となります。

Java on TruffleのGraalVM組込みツールの短いデモを視聴してください。