タグ別アーカイブ: Graal/GraalVM

JVM Language Summit 2019 feedback

例年7月下旬にサンタクララで開催されるJVM Language Summitの報告会を東京(2019/08/09)、大阪(2019/08/23)、福岡(2019/08/30)の3か所で開催しました。

Tapestry of “Project Loom”

これは今年初の試みで、当初はちょっと違う内容を考えていました。ところがきしださんから、以下のようなコメントをもらい、開催した、という背景があります(そのくせ筆者のスライドは全て英語だったわけですが)。

ともかく、3会場で開催し、予想以上の多くの方に参加いただきましてありがとうございました。JVMLSに参加し、スピーカーをやってくださったさくらばさん (@skrb) 、きしださん (@kis) 、さかたさん (@jyukutyo) 、あじさかさん (@ajis_ka) 、そして会場を提供くださったLINE株式会社、LINE Fukuoka株式会社、Yahoo! Japan株式会社のご協力をもって開催できました。厚く御礼申し上げます(ロジ子のオフィスでやらなかった理由は、DevRelチームの手を借りることができなかったからです)。

この3回の報告会で使用したスライドは以下です。

JVMLSはいわゆるお祭り的なカンファレンスではありませんが、狭く深くJVMの一部分に興味をお持ちであれば、結構はまる可能性があります。ご興味ある方は是非来年参加してみてください。

GraalVM 19.2: New Tools

このエントリは以下のエントリをベースにしています。
This entry is based on the folllowing one written by
https://medium.com/graalvm/graalvm-19-2-new-tools-b78a70f54b06

本日GraalVM 19.2がリリースされました。

これは19.1に続くメジャーリリースで、変更点はリリースノートをご覧いただきたいのですが、トピックは以下の通りです。

  • 改善されたprofile-guided optimization
  • LLVM toolchain
  • Java Flight Recorderのサポートを備えたVisualVMの機能強化
  • Visual Studio Codeプラグイン(プレビュー)
  • ネイティブイメージのピークパフォーマンスの改善

GraalVM 19.1: Compiling Faster
https://medium.com/graalvm/graalvm-19-1-compiling-faster-a0041066dee4
https://logico-jp.io/2019/07/06/graalvm-19-1-compiling-faster/
リリースノート
https://www.graalvm.org/docs/release-notes/19_2/#1920

フィードバックや問題を報告くださったコミュニティのみなさま、このリリースにマージされたプルリクエストをお寄せいただいたみなさまに感謝いたします。

IssueとPull request
https://github.com/oracle/graal/issues
https://github.com/oracle/graal/pulls

以後はアップデートやバージョンのロードマップの詳細です。このまま読み続けてくださるもよし、いますぐダウンロードして試すもよし・・・。

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

Profile-Guided Optimizations

JVMで動作するGraalVM just-in-time (JIT) モードは起動時にランタイム情報を収集し、それを使用してマシンコードを最適化する機能があるため、高いピークパフォーマンスを求められる状況に適しています。GraalVM ahead-of-time (AOT)モードは、起動時とメモリ消費量の面で優れていますが、ランタイム情報がないため、ピーク時のパフォーマンスが低下します。このデメリットを低減するために、GraalVMチームはGraalVM Enterprise Editionにプロファイルガイド付き最適化(profile-guided optimizations、PGO)を実装しました。 PGOを使用すると、プロファイリングデータを事前に収集し、それをGraalVMの native-image ユーティリティに送り、この情報を使用してバイナリを生成します。これにより、(PGOなしのバイナリに比べて)パフォーマンスを最適化できます。

GraalVM 19.2までは、PGOを適用するためには、まずインストルメント済みのネイティブイメージバイナリをビルド、実行した上で、プロファイルを収集する必要がありました。この機能を使うことで、アプリケーションをJITモードで実行して収集したプロファイルを使用して高度に最適化されたネイティブバイナリを生成できます。

How to enable PGO

  1.  -Dgraal.PGOInstrument フラグをつけてJITモードでJavaプログラムを実行し、プロファイル情報を収集する。
    $ /graal-ee-19.2.0/bin/java -Dgraal.PGOInstrument=myclass.iprof MyClass
  2. 収集したデータを使ったネイティブイメージを生成する。
    $ /graal-ee-19.2.0/bin/native-image --pgo=myclass.iprof MyClass
  3. 高速起動、高スループットのバイナリイメージの準備完了。
    $ ./myclass

Java Flight Recorder

Java Flight Recorder (JFR) は実行中のJavaアプリケーションの診断データやプロファイルデータを収集するツールです。特定時点でのJVMやJavaアプリケーションで発生するイベントを収集します。

Java Flight Recorder in Graal VisualVM

GraalVMにバンドルされているVisualVMで、Java Flight Recorderファイル (.jfr) のデータを可視化する機能がプレビューで使えるようになりました。

JFRサポートを有効化するには、

  1. まず、 $GRAALVM_HOME/bin/jvisualvm を実行してVisualVMを起動する
  2. Tools > Plugins > Available Plugins を使って利用可能な全てのプラグインをリスト表示し、 VisualVM-JFR と VisualVM-JFR-Generic モジュールをインストールする

JFR スナップショットを File > Load… もしくはJFRスナップショットノードをダブルクリックして開き、スナップショットを永続的にJFRリポジトリに追加します。JFRスナップショットの作成については、お使いのJavaバージョンのドキュメントをご覧ください。

JFRのサポートは現在プレビュー機能です。フィードバックは以下からどうぞ。

VisualVM Feedback
http://visualvm.github.io/feedback.html

バグ、機能リクエストはIssueを立ててください。

GraalVM GitHub Issues
https://github.com/oracle/visualvm/issues

Performance Improvements

すべての構成でパフォーマンスが若干向上しました。 大きなアップデートの一つとして、GraalVM Enterprise Editionのネイティブイメージでデフォルトのスループットが改善されたことがあげられます。GPOなしでMicronautのサンプルをGraalVMで実行したときの結果を測定しました。

Creating your first Micronaut Graal application
https://guides.micronaut.io/micronaut-creating-first-graal-app/guide/index.html

19.2では、100万のリクエストに対して、CPU時間が最大10%短縮し、単位時間あたりにおけるCPU1個あたりのリクエストのスループットが最大10%向上しています(訳注:原文はCPU secondという表現を使っています。CPUあたりという表現もおそらく1コアあたりと表現すべきでしょうが、詳細不明のため、CPUあたりとしています)。GraalVMのjust-in-time (JIT) 構成に比べるとスループットはまだ低いですが、AOT構成を同じレベルにするための進歩を続けています。

LLVM Toolchain

GraalVMには LLVM bitcodeエンジンを同梱しており、このエンジンを使ってLLVM bitcodeにコンパイル可能なC/C++やその他の言語を実行できます。Although compiling C/C++をbitcodeにコンパイルすること自体は、clangやllvm-linkといった標準的なLLVMツールで可能ですが、特にmakeのようなリンク・ビルドシステムが登場する場合は注意が必要です。GraalVM LLVMランタイムを対象とするネイティブプロジェクトを構築する際に、コンパイラを単純に置き換えてすぐ使える機能をtoolchainで提供することにより、このプロセスを簡素化することを目指しています。

How to get started

GraalVM 19.2をダウンロードし、LLVM toolchainを以下のコマンドを使って追加します(Community EditionとEnterprise Editionで違いはありません)。

gu install llvm-toolchain

toolchainを使って”Hello World!”のC++プログラムを実行します。

$ $GRAALVM_HOME/jre/languages/llvm/native/bin/clang++ hello-c++.cpp -o hello
$ $GRAALVM_HOME/bin/lli hello

LLVM toolchainの詳細を知りたい方は以下のドキュメントをご覧ください。

Running LLVM on GraalVM
https://www.graalvm.org/docs/reference-manual/languages/llvm/

toolchainは現時点ではまだ実験的 (experimental) であることにご注意ください。機能の追加、変更、削除の可能性があります。

GraalVM Extension for Visual Studio Code

実験的なVS Code extensionの第1版をリリースしました。このextensionはGraalVMで動作するプログラムの編集とデバッグの基本的なサポートを提供します。

GraalVM Support for VS Code
https://github.com/oracle/graal/tree/master/vscode/graalvm

以下のデバッグ設定を利用できます。

  • Node.jsアプリケーションの起動
  • JavaScriptの起動
    GraalVMを使うJavaScriptをデバッグモードで起動します。
  • アタッチ
    ローカルで実行中のGraalVMにデバッガをアタッチします。
  • リモートへのアタッチ
    リモートのGraalVMのデバッグポートにデバッガをアタッチします。

エディターで開かれたJavaScriptソースの場合、すべての Polyglot.eval(…) 呼び出しが検出され、それぞれの埋め込み言語がその場所に挿入されます。

引き続きこのプロジェクトに取り組んでおりますので、フィードバックやご提案を承っております。

Version Roadmap

予測可能なリリース日とアップデートリリースに関する情報を含むバージョンロードマップの説明を作成しました。以下のURLにあるリリースツリーの図の下にあります。

Version Roadmap
https://www.graalvm.org/docs/release-notes/version-roadmap/

将来的には、次のメジャーリリースで計画されている機能を追跡できるプロセスを導入する予定にしています。


GraalVM 19.2をダウンロードして新機能をお試しください。

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

フィードバックをお待ちしています。GraalVMのGitHubリポジトリにIssueを立ててもらってもいいですし、何らかの方法でメッセージをお寄せいただいても結構です。

GraalVM GitHubリポジトリ
https://github.com/oracle/graal
GraalVM Community
https://www.graalvm.org/community/

libgraal: GraalVM compiler as a precompiled GraalVM native image

このエントリは以下のエントリをベースにしています。
This entry is based on the following one written by Doug Simon (Manager with Oracle Labs).
https://medium.com/graalvm/libgraal-graalvm-compiler-as-a-precompiled-graalvm-native-image-26e354bee5c

また、本来であればコンパイラをGraalといいますが、原文ではGraalVMコンパイラと表現しています。今回は原文に合わせてGraalVMコンパイラと表現しています。

このエントリでは最近のGraalVMのアップデートであるlibgraalについて説明します。これはGraalVM Native Imageが生成する共有ライブラリで、この中にはコンパイル済みのGraalVMコンパイラが含まれています。JavaアプリケーションをGraalVM上で実行する場合、最上位層のJITコンパイラとしてlibgraalを利用します。

libgraal
https://github.com/oracle/graal/tree/master/compiler#libgraal
GraalVM Native Image
https://www.graalvm.org/docs/reference-manual/aot-compilation/

libgraalには様々な便益があります。例えばlibraalは起動速度を改善し、完全にヒープ利用なアプリケーションコードのプロファイリングによるオーバーヘッドを完全に回避します。つまり、コンパイラはようやく “codes like Java, runs like C++” (Javaのようなコードで、実行はC++のように)になりました。HotSpotの文脈でより具体的に言えば、libgraalはマネージドランタイムのほとんどのメリットを生かしながら、C2のように実行します。libgraalはGraalVM 19.1リリースで短時間および中時間動作するワークロードのコンパイル速度とパフォーマンスの改善に大きく貢献しています。詳細は以下のエントリをご覧ください。

GraalVM 19.1: Compiling Faster
https://medium.com/graalvm/graalvm-19-1-compiling-faster-a0041066dee4
https://logico-jp.io/2019/07/06/graalvm-19-1-compiling-faster/

The GraalVM compiler and native-image

まず、GraalVMコンパイラとnative-imageの関係に不慣れな人のために、背景をご紹介しましょう。コンパイラは、Javaバイトコードをマシンコードにコンパイルするために使用します。HotSpotをJITコンパイラとして使用する場合、アプリケーションのうち頻繁に実行される(すなわち、ホットな)バイトコードをコンパイルするだけです。native-imageツールは、GraalVMコンパイラを使用してJavaバイトコードをマシンコードにコンパイルしますが、アプリケーションのすべてのバイトコードを事前コンパイルします。GraalVMコンパイラ自体がJavaで書かれているので、ネイティブイメージの観点からGraalVMをアプリケーションとして扱うことができます。このようにして、実行時にコンパイル済みのマシンコードとしてすぐに実行できるバージョンのコンパイラが得られます。

Getting started with libgraal

JVM上で実行する場合、GraalVMのいくつかのリリースから、GraalVMコンパイラのlibgraalはデフォルトモードです。つまり、java ランチャーや任意の言語ランチャーを –jvm オプションを付けて利用する場合、全ての最上位層のコンパイルはlibgraalを使って実行されます。(現在の)レガシーモードからこの実行モードを厳密に表現するために、後者についてはjargraalという用語を使います。さらに、文脈が明らかな場合は、簡潔にするためにGraalVMコンパイラを単に「コンパイラ」と呼びます。

Warmup improvements

libgraalの主要なメリットは、コンパイルが最初から速いことです。これはコンパイラが最初からコンパイル済みの状態で実行され、HotSpotインタプリタを完全に迂回するからです。さらに、libgraalは単体でコンパイルされています。対して、jargraalの場合、C1でコンパイルされます。その結果、jargraalに比べてlibgraalではTコンパイラのコンパイル済みコードはより最適化されています。

では、GraalVM Enterprise EditionとCountUppercaseをサンプルとして使って、これら全てがどれほど起動スピードのパフォーマンス向上に寄与しているか説明します。

GraalVM demos: Performance Examples for Java
https://www.graalvm.org/docs/examples/java-performance-examples/

java CountUppercase On your marks, Get set, Go...
1 (191 ms)
2 (107 ms)
3 (69 ms)
4 (120 ms)
5 (27 ms)
6 (26 ms)
7 (27 ms)
8 (28 ms)
9 (27 ms)
total: 29999997 (651 ms)

jargraalと比較するため、 -XX:-UseJVMCINativeCompiler オプションを使います。

java -XX:-UseJVMCINativeLibrary CountUppercase On your marks, Get set, Go...
1 (1065 ms)
2 (329 ms)
3 (149 ms)
4 (107 ms)
5 (106 ms)
6 (81 ms)
7 (125 ms)
8 (51 ms)
9 (34 ms)
total: 29999997 (2081 ms)

AOT (ahead-of-time、事前) コンパイルされたGraalVMコンパイラの利点は明確です。この例では、jargraalの場合 (1.5秒後) に比べてlibgraalでは約3倍速く (0.5秒後) ピークパフォーマンスに到達します。GraalVM CEのlibgraalでも同様のウォームアップ時間を獲得します。

より直接libgraalのコンパイル速度を測定するために、-XX:+CITime フラグを使います。これを使うと、他のメトリックと共に1秒間あたりのコンパイルデータ量(バイト) を出力します。このデータ量にはインラインメソッドのバイト数も含みます。このフラグを付けてCountUppercaseを実行すると、libgraalでは74kB/secほどコンパイルしていたのに対し、jargraalでは9kB/secほどでした。比較のため、C1は約380kB/sec、C2は約80kB/secほどコンパイルします。プロファイルガイド最適化(PGO)を使用すると、libgraalはC2のコンパイル速度を上回ることが想定されます。

ウォームアップが短時間で済むことに加え、HotSpotヒープからGraalへ移行するその他のメリットをご紹介します。

Memory improvements

jargraalモードでは、コンパイラを(jarファイルにデプロイ済みの)クラスファイルからロードし、JVMの他のクラスと同様に実行します。メモリ割り当てはアプリケーションコードが利用するものと同じGCされたヒープで行われます。さらに、コンパイラクラスはHotSpotのmetaspace(クラスやメソッド、プロファイルといったメタデータのために使われる管理対象メモリ領域)を占有します。これにより数多くの問題を発生する可能性があります。

  • GraalVMコンパイラのヒープ要件を考慮する必要があるため、アプリケーションのヒープ要件の計算が難しくなります。
  • コンパイラヒープオブジェクトをアプリケーションヒープオブジェクトとインターリーブすることで、オブジェクトの局所性を乱すことがあります。これはパフォーマンスに直接影響する可能性があります。
  • コンパイラーによるメモリ割り当てのために速くヒープがいっぱいになるため、ガベージコレクションの実行回数が増えます。

こうしたメモリへの作用をJava Mission Controlで確認できます。以下はCountUppercaseをjargraalで実行した際のメモリ利用状況を示すスクリーンショットです。

Memory usage for jargraal

茶色の棒は測定期間中に実行された11回GCが発生したことを示しています。紫の線はヒープの利用量を示しています。コレクションの1つにマウスをホバリングすると、GC後に使われているメモリ量がわかります。

Live memory after collection (jargraal)

対して、以下はlibgraalのメモリ利用のプロファイルです。

Memory usage for libgraal
Live memory after collection (libgraal)

ここからは、libgraalの場合、GCは4回しか発生していないことがわかります。libgraalプロファイルではGC後2MBのライブメモリーしかないlibgraalプロファイル内のコレクションの後にはわずか2 MBのライブメモリーしかないため、比較するとjargraalが保持する約7.5 MBのライブメモリーを削減できることがわかります。

Profile pollution

jargraalの別の副作用として、コンパイラの実行によって、アプリケーションが使用するコードのプロファイルが乱される可能性があります。例えば、java.util.HashMapを使用し、かつString型のキーのみを使用するアプリケーションを考えてみましょう。HashMap.putAll(Map m)の呼び出しでは、Object.hashCodeの呼び出しの型プロファイルは、mのキーが常にString型であることを示します。ただし、コンパイラはHashMapも使用しており、常にStringキーを使用するわけではありません。コンパイラがHashMap.putAllを呼び出すと、これらの他の型で型プロファイルが「汚染」されるため、HashMap.putAllのString.hashCodeメソッドがインライン化されなくなる可能性があります。

この手の型汚染 (type pollution) をGraalVM Enterprise Editionが実施するように積極的なインライン化によって軽減できますが、完全にこれをなくすには、プロファイルを更新しないモードでコンパイラを実行するしかありません。現時点までこのことを予測していなかった場合は、libgraalがまさにこのモードを提供します。さらに、libgraalではコンパイルは発生しないので、GraalVMコンパイラをプロファイルする必要は全くありません。

Advantages of Java

Javaで書かれ、マシンコードにコンパイルされているため、libgraalはjargraalのほとんどの利点を保持します。例えば…

  • 圧縮参照 (compressed references) :以前のGraalVMの記事で説明したように、native-imageは圧縮ポインタをサポートしています。この機能の要約は、libgraal内のすべてのオブジェクトポインタを64ビットではなく32ビットで表現できるため、メモリを大幅に節約できます。

    Isolates and Compressed References: More Flexible and Efficient Memory Management via GraalVM
    https://medium.com/graalvm/isolates-and-compressed-references-more-flexible-and-efficient-memory-management-for-graalvm-a044cc50b67e
    https://logico-jp.io/2019/03/27/isolates-and-compressed-references-more-flexible-and-efficient-memory-management-via-graalvm/
  • Garbage collection:C1やC2のようなHotSpotのネイティブコンパイラはコンパイル中メモリを割り当て、そのメモリの解放は通常コンパイル終了後です。libgraalはGCをサポートするネイティブイメージで動作するため、コンパイラが利用するメモリ量に上限を設定するヒープサイズを付けて実行するよう構成でき、特定のクラスのコンパイラのバグでVMを破壊することを防ぎます。コンパイルへのトップレベルのエントリポイントは、OutOfMemoryErrorをキャッチして適切なアクション(たとえば、コンパイルの中止)を実行するための例外ハンドラをインストールします。対照的に、過剰な割り当てをもたらすC1またはC2のバグは、検出不可能なメモリ不足エラーでVMプロセスを強制終了させることになります。
  • Robustness against compiler bugs (コンパイラバグに対する堅牢性):これまでのポイントを一般化すると、例外をもたらすコンパイラのバグは、そのダメージを軽減できます。コンパイルされていたメソッドのコンパイル済みコードがなくても、例外をキャッチしてVMを実行し続けることができます。-Dgraal.CompilationFailureAction=Diagnoseオプションを使用すると、このような失敗でさえも、バグレポートと一緒に送信できる有用な診断情報を生成できます。-Dgraal.CrashAtオプションを使って、CountUppercaseの例でこれをシミュレートできます。
java -Dgraal.CrashAt=equals -Dgraal.CompilationFailureAction=Diagnose CountUppercase On your marks, Get set, Go...
-- iteration 1 --
1 (246 ms)
Thread[System-0,5,main]: Compilation of java.lang.String.equals(Object) failed:
java.lang.RuntimeException: Forced crash after compiling java.lang.String.equals(Object)
at org.graalvm.compiler.core.GraalCompiler.checkForRequestedCrash(GraalCompiler.java:198)
at org.graalvm.compiler.core.GraalCompiler.compile(GraalCompiler.java:152)
at org.graalvm.compiler.core.GraalCompiler.compileGraph(GraalCompiler.java:129)
at org.graalvm.compiler.hotspot.HotSpotGraalCompiler.compileHelper(HotSpotGraalCompiler.java:212)
at org.graalvm.compiler.hotspot.HotSpotGraalCompiler.compile(HotSpotGraalCompiler.java:226)
at org.graalvm.compiler.hotspot.CompilationTask$HotSpotCompilationWrapper.performCompilation(CompilationTask.java:186)
at org.graalvm.compiler.hotspot.CompilationTask$HotSpotCompilationWrapper.performCompilation(CompilationTask.java:96)
at org.graalvm.compiler.core.CompilationWrapper.run(CompilationWrapper.java:177)
at org.graalvm.compiler.hotspot.CompilationTask.runCompilation(CompilationTask.java:342)
at org.graalvm.compiler.hotspot.HotSpotGraalCompiler.compileMethod(HotSpotGraalCompiler.java:142)
at org.graalvm.compiler.hotspot.HotSpotGraalCompiler.compileMethod(HotSpotGraalCompiler.java:108)
at jdk.vm.ci.hotspot.HotSpotJVMCIRuntime.compileMethod(HotSpotJVMCIRuntime.java:663)
at com.oracle.svm.jni.JNIJavaCallWrappers.jniInvoke_VA_LIST_Nonvirtual:Ljdk_vm_ci_hotspot_HotSpotJVMCIRuntime_2_0002ecompileMethod_00028Ljdk_vm_ci_hotspot_HotSpotResolvedJavaMethod_2IJI_00029Ljdk_vm_ci_hotspot_HotSpotCompilationRequestResult_2(JNIJavaCallWrappers.java:0)
To disable compilation failure notifications, set CompilationFailureAction to Silent (e.g., -Dgraal.CompilationFailureAction=Silent).
To print a message for a compilation failure without retrying the compilation, set CompilationFailureAction to Print (e.g., -Dgraal.CompilationFailureAction=Print).
Retrying compilation of java.lang.String.equals(Object)
Dumping IGV graphs in /Users/dnsimon/graal/graal/compiler/graal_dumps/1555858467364/graal_diagnostics_41644/java.lang.String.equals(Object)
2 (103 ms)
3 (70 ms)
4 (135 ms)
5 (26 ms)
6 (27 ms)
7 (26 ms)
8 (26 ms)
9 (26 ms)
total: 29999997 (712 ms)
Graal diagnostic output saved in /Users/dnsimon/graal/graal/compiler/graal_dumps/1555858467364/graal_diagnostics_41644.zip
  • コンパイラでの過度の再帰を避けることを目指していますが、それでもスタックオーバーフローが発生する可能性があります。native-imageはスタックオーバーフローのチェックをサポートしているので、VMを終了するのではなくコンパイルからの脱出 (compilation bailout) が可能です。

Upcoming updates

libgraalがもたらす機会を活用し、かつ改良する予定があります。その計画の一部をご紹介しましょう。

  • デフォルトでは、native-imageはヒープを利用可能な物理メモリの80%に拡張します。これは、(病的な入力やコンパイラのバグによる)不正なコンパイルが大量のメモリを使い果たし、非常に遅いコンパイルを引き起こす可能性があります。このようなケースを軽減するために、young世代のサイズを調整し、libgraalの最大ネイティブイメージヒープサイズを制限することを現在実験中です。コンパイル時の無制限のメモリ使用を防ぎながら、(コンパイル速度に影響を与える)コレクションの最小化という点で最良のトレードオフを達成する値を見つけることを目指しています。
  • Native Imageでの分離のサポートにより、libgraalのメモリ使用量をさらに減らすことができます。

    Isolates and Compressed References: More Flexible and Efficient Memory Management via GraalVM
    https://medium.com/graalvm/isolates-and-compressed-references-more-flexible-and-efficient-memory-management-for-graalvm-a044cc50b67e
    https://logico-jp.io/2019/03/27/isolates-and-compressed-references-more-flexible-and-efficient-memory-management-via-graalvm/

    GraalVMコンパイラのメモリー使用量を実質的に0にするためにlibgraal分離を完全に破棄できます。これを実行するのに最適な時期は、コンパイル・キューが空のときです。つまり、アプリケーションが安定した状態になると、コンパイラはVMのメモリプロファイルから自分自身を完全に削除できます。コンパイラの初期化時間を1桁ミリ秒という短時間にまで減らすことができれば、各コンパイルごとに新しい分離を作成するモードを提供することも考えられます。これが可能になれば、コンパイラのための絶対的な最小メモリフットプリントを提供するでしょう。各コンパイルがlibgraalの最大ヒープ以下しかメモリを割り当てない限り、libgraalのすべてのガベージコレクションを回避するため、コンパイルが高速になります。
  • libgraalに必要な変更がOpenJDK masterブランチにマージされた後に速やかにJDK 13でlibraalを動作させることに注力していきます。

    [JVMCI] Update JVMCI to support JVMCI based Compiler compiled into shared library
    https://bugs.openjdk.java.net/browse/JDK-8220623
  • GraalVMネイティブイメージチームと協力して、libgraalの静的なフットプリントの削減に取り組みます。これは主にシンボルを減らし、より多くの未使用のコードを削除することが中心になるでしょう。
  • 現時点では、実行する/実行しない最適化のセットでGraalVMコンパイラを構成できますが、現在生成されたコードの品質よりもコンパイル速度を優先するような節約設定の調整に取り組んでいます。これにより、C1の代わりに第1層のコンパイルに利用できる、無駄のないlibgraalを生成できるはずです。

Conclusions

libgraalは共有ライブラリで、コンパイル済みのGraalVMコンパイラが含まれています。これを使えば、起動時のパフォーマンス、競争力のあるピークパフォーマンスが向上し、アプリケーションプロファイルやアプリケーションによるヒープの使用に対するすべての干渉がなくなります。

ぜひGraalVMをダウンロードしてお試しください。

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

優れた機能が不足していることを確認されたり、他に何かフィードバックがある場合には、ご連絡頂くか、GraalVMのGitHubリポジトリにIssueを立てるか、その他の方法でご連絡ください。

GraalVM GitHub repo
https://github.com/oracle/graal
GraalVM Community
https://www.graalvm.org/community

GraalVM 19.1: Compiling Faster

このエントリは以下のエントリをベースにしています。
This entry is based on the following one written by Thomas Wuerthinger (Senior Research Director at Oracle Labs, GraalVM project lead).
https://medium.com/graalvm/graalvm-19-1-compiling-faster-a0041066dee4

機能追加、互換性の向上、バグの修正などが含まれているGraalVM 19.1がリリースされました。是非試していただき、フィードバックをお願いいたします。

GraalVM Download
https://graalvm.org/downloads
Issues
https://github.com/oracle/graal/issues

以前のリリースと同様、coreのダウンロードにはJava 8準拠のJava Virtual Machine (1.8.0_212)、Node.js実装 (v10.15.2)、JavaScript実装 (ECMAScript 2019)、LLVM bitcodeランタイム (6.0.0) が含まれています。オプションのコンポーネントは以下のコマンドで追加できます。

gu install native-image ruby R python

詳細を記載したリリースノートは以下をご覧ください。以後では、GraalVM 19.1の主要な改善点とバージョン番号体系の詳細について説明していきます。

Release Notes (19.1.0)
https://www.graalvm.org/docs/release-notes/#1910

Compiling Faster

just-in-time (JIT) コンパイルシステムの課題の一つとして、コンパイルステップをできる限り高速にする必要があります。先日、Apache Sparkリポジトリからチェックアウトしたものをコンパイルするために、Scala sbtビルドツールをGraalVMをJITモードで使うというサンプルワークロードを分析しました。

sbt – The interactive build tool
https://www.scala-sbt.org/
Apache Spark
https://github.com/apache/spark

このワークロードは3分以上かかるもので、Java Mission Controlを使用してGraalVMのJITコンパイルスレッドの動作を見ると、実行中はコンパイルが走っていることがわかります。

Java Mission Control
https://www.oracle.com/technetwork/java/javaseproducts/mission-control/java-mission-control-1998576.html

Activity of compilation threads for example workload (yellow=occupied).

最終的に最適なマシンコードが実行可能になると、ワークロードはすでに終了しています。このマシンコードを早くインストールすることが、このような中程度の長さのプログラムをより高速に実行するためには重要なので、JITコンパイル速度を向上させるために、いくつかの改善を行いました。具体的には、コンパイラ自体のネイティブイメージを生成する際に、プロファイルガイド付き最適化を使用しています。また、最適化のヒューリスティックを改善して、計算量を減らしました。その結果、コンパイル速度はほぼ2倍になっています。

Improvements for example workload between GraalVM EE 19.0 and GraalVM EE 19.1.

ワークロード終了時、コンパイルによって消費されるCPU時間は約30%少なくなりました。より良いマシンコードを早く入手できるようにすることで、ワークロードの全体の実行時間が13%短縮されます。これは中規模のプログラムにとって重要な改善です。

Versioning

GraalVMプロジェクトは急速に進化し、さまざまなコンポーネントが含まれているため、適切なバージョン管理方法を定めるのは課題になっていました。多言語仮想マシンプロジェクトとしてのGraalVMの進化は急激で、GraalVMがサポートするすべての言語のニーズを受け入れることを望んでいます。開発者が最新のリリースで最新の機能を試すことができるよう、頻繁にリリースしたいと考えています。

現時点では、GraalVMの19という1個のリリースブランチがあり、来年はGraalVM 20、その後はGraalVM 21、という感じでリリースする予定です。その年の間にプロジェクトメインラインから、19.1、19.2、19.3といった具合にいくつかのリリースを出す予定です。予定ではおよそ3ヵ月毎にリリースし、GraalVMが依存するプラットフォーム(OpenJDK、Node.js、LLVM、Ruby、R、Python)へのアップデートに追随していきます。

アップデートリリースはmasterから生成され、最新のものを含む予定です。アップデートプロセスをスムーズにして徐々に新機能を導入したいと考えています。つまり、最初は実験的フラグで主要な変更を隠しておき、その後フラグをデフォルトで有効にする、という流れです。これにより、ユーザーが事前に調整できるようにしながら、プロジェクトの速度を速めることができます。

これらのメジャーリリースに加えて、重要なバグ修正とセキュリティアップデートのみを含むアップデートリリースを作成します。最新のメジャーリリース用にこのようなアップデートリリースを作成し、3桁目で区別します。たとえば、最近、19.0メジャーリリース用に19.0.2アップデートリリースを作成しました。現在、当該年の最終リリースを長期サポート(Long Term Support)できるようにする予定です。つまり、20.0がリリースされた後19.3(もしくは19.4)がリリースされる予定です。正確な日をリリースカレンダーに記載する予定にしていますので、ユーザーの方々は事前に計画できることでしょう。

Outlook

このリリースに対するフィードバックを受け付けていると同時に、今後も進化を続け、既に次のGraalVM 19.2への作業に取りかかっています。

GraalVM Download
https://graalvm.org/downloads
Issues
https://github.com/oracle/graal/issues

Announcing GraalVM 19.0

このエントリは以下のエントリをベースにしています。
This entry is based on the following one, writtern by Oleg Šelajev (Developer advocate for GraalVM at OracleLabs).
https://medium.com/graalvm/announcing-graalvm-19-4590cf354df8

昨年からGraalVMのRelease Candidateビルドを提供してきましたが、これまでにたくさんのフィードバックをいただきました。こうした内容を踏まえ、このたびGraalVM 19.0がリリースされました。いくつかのコンポーネントに分かれており、Coreには以下のコンポーネントが含まれています。

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

Java 8 SE 互換(OpenJDK 1.8.0_212ベース)の Java Virtual Machine

GraalVMコンパイラは、allocation-heavyなワークロードの高速実行に効果があります。Twitterは、GraalVMコンパイラを使用してシステムを実行しています。

[VDM19] Performance tuning Twitter services with Graal and Machine Learning by Chris Thalinger

また、ScalaプログラムはGraalVM上で実行したときにパフォーマンスが大幅に向上します。

Compiling Scala Faster with GraalVM
https://medium.com/graalvm/compiling-scala-faster-with-graalvm-86c5c0857fa3

ES4x for Vert.xは、TechEmpowerベンチマークで選択したランタイムとしてGraalVMを含めました。

ES4x for Eclipse Vert.x
https://reactiverse.io/es4x/
es4x.dockerfile
https://github.com/TechEmpower/FrameworkBenchmarks/blob/cffabe9d29f4bf4b3a9ad845d70726a8ac87fd11/frameworks/JavaScript/es4x/es4x.dockerfile

Renaissance suiteという、JVM上の最新のワークロードをまとめた新しいベンチマークでは、GraalVMを使った実行結果は他のJavaアプリケーション実行方法よりもがよい結果だったことがわかりました。

Renaissance Suite – A benchmark suite for the JVM
https://renaissance.dev/

Node.js v10.15.2ベースのNode.js(polyglot 機能付き)

JavaやScalaライブラリをNode.jsから利用できます。Wixエンジニアリングがこのアプローチについて実験しました。発表内容は以下の動画からどうぞ。

Graal: Using JVM libraries in Node.js – Laurynas Lubys

また、逆にJavaアプリケーションでNode.jsモジュールを使うこともできます。

Mixing NodeJS and OpenJDK
https://blog.plan99.net/vertical-architecture-734495f129c4

最新のECMAScript 2019標準互換のJavaScriptエンジン

もうメンテナンスされないJavaScriptエンジンのRhinoやNashornから最新の標準に準拠するGraalVMのJavaScriptエンジンに以降できます。

ECMAScript 2016+ compatibility table
https://kangax.github.io/compat-table/es2016plus/
Oracle GraalVM announces support for Nashorn migration
https://medium.com/graalvm/oracle-graalvm-announces-support-for-nashorn-migration-c04810d75c1f

LLVM 6.0.0 bitcodeをマネージド環境で実行するためのランタイム

これを使うとネイティブ言語をJavaプログラムにより直接的に統合できます。

Safe and sandboxed execution of native code
https://medium.com/graalvm/safe-and-sandboxed-execution-of-native-code-f6096b35c360

その他のコンポーネントは gu(GraalVM Updater)ユーティリティを使ってインストールできます。

gu install native-image

GraalVM Native Image

現在早期導入機能として利用可能なGraalVM Native Imageは、信じられないほど起動時間が高速になります。これはクラウドへの展開シナリオで非常に歓迎されています。エコシステムの多くのプロジェクトが、成果物やワークロードのプラットフォームとしてGraalVM Native Imageを受け入れています。Quarkus、Micronaut、Helidon ではGraalVM Native Imageをサポートしています。

Quarkus
https://quarkus.io/
Micronaut
https://micronaut.io/
Helidon
https://helidon.io/

Nettyは最近、GraalVM Native Imageのための設定をその成果物に含め、すべてのNettyプロジェクトおよびダウンストリーム・プロジェクトのユーザーがネイティブイメージを生成する作業を簡素化しました。また、Pivotalのエンジニアと共同で作業を続けており、今後Springアプリケーションのネイティブイメージサポートを期待しています。

Netty 4.1.36.Final released
https://netty.io/news/2019/04/30/4-1-36-Final.html

その他の gu ユーティリティでインストール可能なコンポーネントは言語コンポーネントです。

gu install ruby R python

Python、Ruby、Rという言語はOracle Labsで開発しており、このリリースにおいてもなおexperimental(実験的)の状態です。さらなるフィードバックをお待ちするとともに、さらなる安定化やコミュニティ活動が必要です。

しかしながら、GraalVMのpolyglot の性質を探求するプロジェクトはすでにあります。例えばEclipse Vert.x、nodejvmやgKnitは、それぞれJavaとJavaScript、Node.jsとJava、およびRとRubyを混在させるためにGraalVMを使用します。

Eclipse Vert.x
https://vertx.io/
Mixing NodeJS and OpenJDK
https://blog.plan99.net/vertical-architecture-734495f129c4
How to do reproducible research in Ruby with gKnit
https://towardsdatascience.com/how-to-do-reproducible-research-in-ruby-with-gknit-c26d2684d64e

Versioning

これまでGraalVMのマンスリービルドをリリースし、今後プラットフォームの進化・改善を進めていきますが、リリースが公開されたことで、コミュニティがこれまで以上にプラットフォームを信頼するだろうと認識しています。

GraalVM 19.0は、商用利用に推奨する現在の安定版リリースです。少なくとも四半期に1回のペースで、GraalVM 19.1、19.2といった感じでアップデート・リリースを出していく予定です。これらのアップデートにはバグ修正、安定化、性能改善などが含まれますが、メジャーな新機能は含まれません。One-offパッチは必要に応じてリリースしますが、そのバージョンは19.x.1、19.x.2といった具合になります。

同時に、GraalVMの限界を押し広げるために、マンスリーでベータリリースを出す予定です。これにはメジャーな新機能や、新たなプラットフォームのサポートの追加などが予定されています。例えば、エコシステムへの追従と、定期的なプラットフォームのアップデートバージョンに対応したリリースを予定しています。JDK 11ベースのビルドだけでなく、Node.js 12ベースのビルドも計画中です。

ベータリリースは”20.0-betaN”として表現します(Nは増分値)。このベータリリースに含まれる新機能などは、来年リリース予定の次回の安定版リリースであるGraalVM 20.0に含まれる予定です。

Community

日々コミュニティでのGraalVMへの関心が増えていることを認識しており、どんな種類の貢献にも感謝しています。コミュニティとの関わりやプロジェクト全体のガバナンスをさらに改善するために、定期的なcommunity callを実施します。community callの議題は、graalvm-devメーリングリストのメッセージ、GitHubのIssue、もしくはハッシュタグ#GraalVMCommunityのついたTweetに基づきます。つまり、最もいいねが付いたTweetが議題にのるはずです。

graalvm-dev mailing list
https://oss.oracle.com/mailman/listinfo/graalvm-dev

GraalVM Committer Workshopを定期的に開催する予定です。第1回目はCode Oneで実施する予定です。詳細はgraalvm-devメーリングリストで発表します。

我々はGraalVM がエコシステムにもたらす価値を理解しており、さまざまなコントリビュータからの参加を促し、関係者全員にとって最高のバージョンになるように進化していく、素晴らしいオープンソースプロジェクトにしたいと考えています。

Enterprise

GraalVM 19.0 Community Editionのリリースに加えて、GraalVM 19.0 Enterprise EditionもOracleから入手可能です。Enterprise Editionはパフォーマンスを向上させるためのさらに多くのオプションを提供し、商用サポートが付属しています。Enterprise Editionに関する公式ブログ記事は以下からご覧ください。

For Building Programs That Run Faster Anywhere: Oracle GraalVM Enterprise Edition
https://blogs.oracle.com/graalvm/announcement

私たちはGraalVMのすべてのエディションを改良することを約束し、当該製品に対し、サポート対象の商用製品があるというのは、エコシステム全体に価値を付加するものと信じています。これは、企業に対して安定した長期的なサポートを有する製品という選択肢を提供し、今後のコア開発のための資金集めにも有用です。

Outlook

GraalVMチームは、GraalVMをすべてのサポート対象言語に対してクラス最高のランタイムにすることを目指しています。これは野心的な目標であり、時間がかかりますが、GraalVMは現在本番環境で使用でき、多くのシナリオで利点があることがわかっています。我々自身もJavaデプロイメントにGraalVMを使っていますし、TwitterはScalaマイクロサービスをスピードアップするために使っています。GoldmanSachsエンジニアリングチームはGraalVMへの内部プログラミング言語の移行を評価しています。

One VM to Rule Them All? Lessons Learned with Truffle and Graal

WixエンジニアリングチームはJavaとNode.jsのPolyglotアプリケーションを使った実験をしていますし、また軽量なNative Clojureもあります。

clojureD 2019: “Native Clojure with GraalVM” by Jan Stępień

GraalVMを使用するプロジェクトや企業が増えていることは承知しています。そうしたお話をパブリックに伺えることを期待しています。

ぜひGraalVMをダウンロードして開発してください!

Helidon flies faster with GraalVM

このエントリは以下のエントリをベースにしています。
This entry is based on the following written by Dmitry Kornilov.
https://dmitrykornilov.net/2019/04/17/helidon-flies-faster-with-graalvm/

GraalVMはOracle Labsが開発した高性能なオープンソースのPolyglot仮想マシンです。GraalVMは複数の機能を有しており、その中にJavaコードをAhead of Timeコンパイルしてネイティブ実行バイナリにする機能があります。バイナリはJavaランタイムを必要とせず、OS上でネイティブに動作します。

GraalVM
https://www.graalvm.org/

ネイティブ実行機能により、重要な便益がもたらされます。短時間での起動ならびにメモリフットプリントの削減、さらにコンテナ内でネイティブ実行コードを動作させる場合、コンテナイメージにJavaランタイムを含まないため、(同じJavaアプリケーションをこれまでのJVMで動作させる場合に比較して) コンテナイメージのサイズが小さくなります。最適化されたコンテナのサイズはクラウドにアプリケーションを展開する上で重要です。

Helidon 1.0.3からGraalVMのネイティブイメージ機能をサポートするようになりました。これでHelidonアプリケーションをネイティブ実行イメージとしてコンパイルし、前述のメリットを享受できるようになりました。例えば、このエントリで紹介するサンプルアプリケーションの起動時間は0.1msec、macOS上での実行ファイルのサイズは21MBでした。これらの数値はこれまでのJVMを使っている場合よりもよい結果です。

対して、すべてのものごとにはトレードオフがあります。JVM上で長時間実行するアプリケーションはランタイム最適化のおかげでGraalVMのネイティブ実行イメージより高パフォーマンスです。ここでのキーワードは長時間実行(long-running)で、短時間実行(short-running)するアプリケーション、例えばserverless functionのようなものはネイティブ実行によって性能面でのメリットを享受できます。それゆえ、高速な起動時間と小さなイメージサイズ(そしてネイティブ実行イメージ作成に必要な追加のステップ)を求めるのか、それとも長時間実行するアプリケーションでの性能を求めるのか、自問自答し結論を出す必要があります。

ネイティブバイナリへのコンパイルにあたり、アプリケーションに一定の制約が加わります。ネイティブコンパイルのため、リフレクションを使っているコード内のすべてのポイントを識別する必要があり、CDIランタイムインジェクションを使っているコードでは利用できません。Helidon MPはMicroProfile標準をサポートしており、この標準ではCDI 2.0を必要としていますし、Helidon CDI Cloud ExtensionsはCDIプラグインとして実行されています。Helidon MPでユーザー体験を制限したり複雑にしたりしたくはありませんので、Helidon SEでのみ
GraalVMをサポートします。Helidon SEは小さな、リアクティブマイクロサービスを構築するために設計されているため、ちょうどよいのです。依存性の注入や注釈、その他そういった魔法は使いません。すべてのHelidon SEの機能とコンポーネント(WebServer Config、Security、Metrics、Health Checks)はGraalVMネイティブイメージと互換性があります。

Helidonは2個の便利なGraalVMプロファイルをサポートします。

  • local profile:GraalVMをローカル環境にインストールしており、ローカル環境と同じOS上で動作するネイティブ実行イメージをビルドしたい人向け
  • Docker profile:GraalVMをローカル環境にインストールしていない人、もしくはローカルではmacOSを使っているものの、Linux用のネイティブ実行イメージをビルドしたい人向け

最終リリースまでにGraalVMに後方互換性のない変更が行われる可能性があるため、HelidonでのGraalVMのサポートは実験的なものです。GraalVMバージョンRC13でテスト済みです。 他のGraalVMバージョンで動作することは保証されていません。

GraalVM Community Edition 1.0 RC13
https://github.com/oracle/graal/releases/tag/vm-1.0.0-rc13

HelidonにはQuickStartのサンプルがあるので、これを使ってみましょう。このアプリケーションをHelidon on GraalVMアプリケーションのテンプレートとして利用することもできます。

Guides — Quickstart SE
https://helidon.io/docs/latest/#/guides/02_quickstart-se

Mavenのachetypeを使ってプロジェクトを生成します。

mvn archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-se \
    -DarchetypeVersion=1.0.3 \
    -DgroupId=io.helidon.examples \
    -DartifactId=helidon-quickstart \
    -Dpackage=io.helidon.examples.quickstart

ローカルプロファイルを使ってビルドするには、GraalVMをダウンロード、展開して展開先を環境変数GRAALVM_HOMEに設定する必要があります。

macOSを使っている場合、以下のようにGRAALVM_HOMEはGraalVMのルート内の <展開先>/Contents/Homeディレクトリを指し示している必要があります。

export GRAALVM_HOME=~/graalvm-ce-1.0.0-rc13/Contents/Home

プロジェクトをビルドして…

mvn package -Pnative-image

実行しましょう。

./target/helidon-quickstart

Dockerプロファイルを使いたい場合、GraalVMのインストールは必要ありません。以下のコマンドでDockerイメージを作成できます。

docker build -t helidon-native -f Dockerfile.native .

実行は以下のように。

docker run --rm -p 8080:8080 helidon-native:latest

Helidonはマイクロサービス用にゼロから作り上げたJavaのフレームワークであり、小さく効率的なJavaアプリケーションを作成するために必要なすべてのものを提供しています。HelidonはreactiveとMicroProfileをサポートするimperative(命令型)の2種類のプログラミングモデルを用意しています。

With GraalVMのサポートが加わり、Helidonはクラウドネイティブなマイクロサービスを開発するためのベストなソリューションの一つになりました。

ぜひHelidonオープンソースコミュニティに参加してください。ユーザーのみなさまからの質問やみなさまへのサポートをするための準備ができています。お気軽にどうぞ。

Resources

Isolates and Compressed References: More Flexible and Efficient Memory Management via GraalVM…

このエントリは以下のエントリをベースにしています。
This entry is based on the one written by Christian Wimmer, VM and compiler researcher at Oracle Labs. Project lead for GraalVM native image generation (Substrate VM).
https://medium.com/graalvm/isolates-and-compressed-references-more-flexible-and-efficient-memory-management-for-graalvm-a044cc50b67e

tl;dr: GraalVMのネイティブイメージでは、isolate(分離/同一プロセス内の複数の独立したVMインスタンス)と圧縮参照(64ビットアーキテクチャ上のJavaオブジェクトへの32ビット参照を使用)をサポートするようになりました。これにより、メモリの占有スペースが削減され、メモリを厳密に分離することが可能になります。 例えばさまざまなユーザーからのWebリクエストに対してメモリを分離できるようになります。

Introduction

多くの人が、JavaとVirtualBoxの両方が全く異なることをしているにもかかわらず、なぜ「仮想マシン (virtual machines) 」と呼ばれるのか疑問に思ってらっしゃいます。よくある回答の1つに、どちらの「仮想マシン」も

  • プログラムに対して「write-once, run-anywhere」を実現しようとしている
  • プログラムの配下にあるマシンを隠そうとしている

というものがありますが、いずれもまったく異なる方法で実現しようとしています。ただし、VirtualBoxのような仮想マシンを使用する主な理由(OSレベルの仮想マシンと呼びましょう)は、物理的なハードウェアリソースをより効率的に使用するため、同一サーバー上の複数のアプリケーションをまとめて他と干渉しないよう隔離するためです。DockerのようなOSレベルのVMの新バージョンは、複数のアプリケーション間でOSを共有することでより効率的な仮想化を実現しています。

GraalVMでは、私たちが「言語レベル」の仮想化と呼ぶ、さらに別の種類の仮想化を提供します。これは複数の言語を同じプロセス(またはスレッド)で実行できるようにする、というものです。ある言語で書かれたライブラリーを、パフォーマンスの犠牲なく別の言語から直接呼び出すことができるようにすることで、別のレベルの「write-once, run-anywhere」を提供します。本日、GraalVMが、複数のアプリケーションがJVMのような言語ランタイムを共有できるようにすることで、ハードウェアリソースをさらに効率的に使用できる方法を提供するようになったことをお知らせします。これは、クラウド環境や、サーバーあたりのテナント数を増やすことでインフラストラクチャコストを直接削減できる環境においては重要になるでしょう。時間が経つにつれて、GraalVMはJavaスタイルとOSレベルの仮想化の境界線を曖昧にし続けると予想されます。

この目的のため、GraalVMにはisolate (分離) と呼ばれる新しい仮想化機能を導入しています。GraalVMのisolateとは、同じVMインスタンス内の複数のタスクを独立して実行することを可能にする、互いに素なヒープです。従来のJavaアプリケーションサーバーでは、すべてのタスクが同じメモリヒープを共有しています。1つのタスクが大量のメモリを使用すると、ガベージコレクション(GC)が発生し、そのヒープを共有している他のタスクの速度が低下します。isolateは明らかにばらばらなので、各isolateを独立してガベージコレクションできます(またはGCが必要になる前に破棄することができます)。isolateは、アプリケーションのマルチテナント管理や、単一のモノリシックアプリケーションを管理可能なマイクロサービスに分割したりするための優れたツールです。

Isolateによって、圧縮ポインタという別の最適化も可能になります。isolateヒープは通常1つのタスクだけで使用されることを目的としているため(Javaアプリケーションサーバーがサポートを求められる可能性のある大量のタスクではありません)、Isolateは小さくてよく、isolateヒープに含まれるすべてのメモリをアドレッシングするための64ビットポインタを必要としません。オブジェクト指向言語のほとんどのデータ構造は、プリミティブデータよりもポインタのためのスペースを多く使用するため、ポインタのためのスペースを小さくできると、アプリケーションのフットプリントに大きな影響を与える可能性があります。

万事OKのように聞こえますが、isolateには注意すべき重要な制限があります。第1に、データベースやサーバーレスクラウドなどのような、タスクを管理する上位レベルのテクノロジが使用するよう設計された低レベルの機能である、という点です。例えばデータベースの場合、Graalコンパイラはそれ自身のオブジェクトとアプリケーションデータを配置するために、それぞれisolateを使います。第2に、isolateには、本格的な仮想化テクノロジに期待されるスナップショットなどの機能がまだ含まれていません。最後に、isolateと圧縮ポインタはSubstrate VMでのみ利用可能、という点です。換言すると、Java HotSpot VMでは独自のヒープ管理をしているため、Java HotSpot VMに組み込まれたGraalVMとして実行する場合には利用できません。isolateはGraalVMの任意のエディションでも利用できますが、圧縮ポインタはEnterprise Editionでしか利用できません。

Isolateは、メモリ使用量を削減するようにサービスを構築するための強力なツールであり、最大遅延やスループットなどの他の指標にもプラスの効果をもたらします。これは、サービスがJavaやScalaで作成され、事前コンパイル(Ahead-of-Time compilation)されるGraalVMの native-imageツールと組み合わせて使用​​できます(起動時間だけでなく、メモリ使用量も削減されます)。

Ahead-of-time Compilation
https://www.graalvm.org/docs/reference-manual/aot-compilation/

この件は以前のエントリで詳説しています。

Understanding Class Initialization in GraalVM Native Image Generation
https://medium.com/graalvm/understanding-class-initialization-in-graalvm-native-image-generation-d765b7e4d6ed

あるいは、isolateを使ってTruffle API上に構築された動的言語(JavaScript、Pythonなど)を管理することもできます。これはGraalVMをデータベースに埋め込むときに使っている方法です。

isolateの最も一般的な使用法として、リクエストごとに別々のisolateを使うマルチテナントサーバー構築時を想定しています。リクエスト処理がすめば、isolateは、ガベージコレクションを実行せずに単純に破棄できます。このアプローチは、Webサーバーのようなイベントを並行処理するアプリケーションに最適です。このエントリでは、isolateを使ってそのようなタイプのアプリケーションを構築する方法の詳細を説明します。

Isolates

isolateは、同じプロセス内に複数の独立したVMインスタンスを提供します。isolateを作成すると、起点としてイメージヒープを持つ、新しいヒープを作成します。これは、イメージ生成中に行われるすべての初期化が、すべてのisolateで即時に使用可能になることを意味します。すべてのisolateは、事前コンパイルされた同じコードを共有します。つまり、isolateごとに個別の静的分析やコンパイルは実施しません。コードは不変なので、このコード共有は望ましいものです。

各isolateは別々のヒープを持っているので、2つのisolate間でJavaオブジェクトの直接参照を持つことはできません。これは制限であると同時に利点でもあります。アプリケーション開発者は、オブジェクトグラフが完全にパーティション化されていることを確認する必要があります。例えば、すべてのisolateからアクセス可能なグローバルキャッシュを持つことはできませんが、isolateを使うと、各isolateにおいて独立してガベージコレクションを行うことができます。つまり、他のisolateを停止したり影響を与えたりすることはありません。isolateによって割り当てられたすべてのメモリは、isolateが破棄されると自動的に解放されます。このときガベージコレクションは必要ありません。

メモリの分離により、セキュリティの保証ももたらします。つまり、(おそらく特定のユーザーと関連付いている)あるisolateのオブジェクトに対し、(他のユーザーと関連付いている)別のisolateが誤ってアクセスすることはできません。静的フィールドまたはライブラリが維持しているキャッシュを介してユーザ間で情報を漏らしてしまうようなバグは、isolateによって防止されます。

下図は、プロセス内の2つのisolateを示しています。各isolateには、個々のイメージヒープのコピー(メモリ使用量を削減するために copy-on-writeマッピングを使用して効率的に管理)と、新しく割り当てられたオブジェクトが配置される独自のランタイムヒープがあります。

isolateを作成、管理するためのAPIとして2種類のAPI、つまりJava APIとC APIを提供します。Java APIはJavaでのみ(もしくはScalaやKotlinで)作られているアプリケーションで複数のisolateを使いたい場合の利用を想定しています。C APIは既存のCアプリケーションとJava(やScala、Kotlin)のコードを統合し、Cのコードでisolateを管理したい場合の利用を想定しています。まずJava API、続いてC APIの概要をご紹介します。繰り返しになりますが、Java APIとC APIの両方ともネイティブイメージでのみ利用できます。Java HotSpot VMのようなJava VM上では利用できませんのでご注意ください。

Javaのmain()メソッドを持つ実行可能アプリケーションを構築すると、main()メソッドが呼び出される前にデフォルトのisolateを自動作成します。Cのコードと統合されている共有ライブラリを構築する場合、isolateは自動作成されません。つまり、最初のisolateはC APIを使用して作成する必要があります。

Java API for Isolates

APIでは、IsolateIsolateThreadという2つのOpaqueなポインタ型を導入しています。

Interface Isolate
https://www.graalvm.org/sdk/javadoc/org/graalvm/nativeimage/Isolate.html

Interface IsolateThread
 https://www.graalvm.org/sdk/javadoc/org/graalvm/nativeimage/IsolateThread.html

これらはJavaインターフェースのように見えますが、実際は機械語サイズの値であって、Javaオブジェクトではありません。詳細はこのエントリの範囲外ですが、単純にCのvoid*ポインタとしてこれらの値を考えてください。

Isolate はisolateのメインディスクリプタであり、この値を持っていれば、isolateにフルアクセスできます。isolateにアタッチする各スレッドをIsolateThreadで表現します。これはよく使われる型で、isolateのメソッドを呼び出したい場合、IsolateThreadを渡す必要があります。

Isolatesクラスにはisolateを管理するためのAPIが含まれていて、ここにisolateのライフサイクルを管理するための重要なメソッドがあります。

IsolateThread createIsolate(CreateIsolateParameters params);
void tearDownIsolate(IsolateThread thread);

メソッドcreateIsolate()は、新しい独立VM​​インスタンスを作成、初期化します。新しいisolateのJavaヒープはイメージ・ヒープのみで構成、つまり、呼び出し元isolateのオブジェクトは新しいisolateでは使用できません。現在のスレッドは新しいisolateにアタッチされ、IsolateThreadディスクリプタは呼び出し元isolateに返されます。この後、新しいisolateのメソッドを呼び出すことができます。メソッドtearDownIsolate()はisolateを破棄します。とりわけ、isolateに関連するすべてのメモリをOSに返すことで解放するため、ガベージコレクションは必要ありません。

スレッドのアタッチ、デタッチ、スレッドのアタッチ済みかどうかのチェック、IsolateIsolateThread間の変換するための機能もあります。詳細はAPI仕様をご覧ください。

Class Isolates
https://www.graalvm.org/sdk/javadoc/org/graalvm/nativeimage/Isolates.html

このエントリの実行例を紹介しましょう。Netty Webサーバーを使用して、関数のプロットを求めるWebリクエストに応答しています。

Netty
https://netty.io/

この関数は、httpリクエスト内でユーザーが指示します。httpレスポンスはScalable Vector Graphics(SVG)オブジェクトです。式の評価にはexp4jを、SVGファイルのレンダリングにはSVGGraphics2Dを使います。

exp4j
https://www.objecthunter.net/exp4j/
SVGGraphics2D
http://www.jfree.org/jfreesvg/javadoc/org/jfree/graphics2d/svg/SVGGraphics2D.html

式の評価とレンダリングの両方で一時的なJavaデータ構造が割り当てられます。これらのオブジェクトはすべて、リクエスト処理後は到達不能になります。しかし従来のJava VMの場合、これらの一時オブジェクトを最終的に破棄するには、依然として高価なガベージコレクションが必要です。httpリクエストは様々なユーザーから到着するため、依存しているすべてのライブラリを完全にコードレビューしなければ、あるユーザーが他のユーザーの式のプロパティを観察できるようなグローバルデータ構造やキャッシュがあるかどうかはわかりません。isolateは両方の問題を解決します。リクエストハンドラでリクエストごとに新しいisolateを作成し、式の評価後にそのisolateを破棄するからです。

今回は実際のレンダリング機能の呼び出しを追加する必要がありますが、これは通常のJavaメソッド呼び出しよりも少し複雑です。というのも、Nettyのisolate(アプリケーションの起動時に自動的に作成されたデフォルトのisolate)を離れ、レンダリングのisolate(明示的に作成した新しいisolate)に入る必要があるためです。また、2つのisolateのヒープは完全に分離されているため、Javaオブジェクトを直接渡すことはできません。文字列引数functionはNettyのisolate内のオブジェクトなので、レンダリングのisolateからはアクセスできません。最初に文字列をレンダリングのisolateにコピーする必要があります。戻り値についても同じことが言えます。JavaオブジェクトByteBufferを直接返すことはできず、Javaオブジェクトへのハンドルのみを渡すことができます。「ハンドル」とは、Javaオブジェクトへの不透明な間接指定です。ハンドルが参照するオブジェクトは、そのオブジェクトとハンドルが作成されたisolate内でのみアクセスできます。

結果として、今回のレンダリングのメソッドを以下のように定義します。

アノテーション @CEntryPoint は、メソッドをisolate-transition (分離の遷移) メソッドとしてマークします。このメソッドをCのコードから直接呼び出すこともできます(注釈名が由来するところです)。パラメータ renderingContext は関数が呼び出された際に入るisolateです。これはパラメータ・アノテーション @CEntryPoint.IsolateThreadContext で表されます。後にNettyのisolateにコールバックする必要があるため、パラメータとしてnettyContextも渡しますが、当該パラメータに特別な意味はありません。

パラメータ functionHandleと戻り値はObjectHandleという型です。各ハンドルごとに、どのisolateで有効かを知る必要があります。そこで、レンダリングのisolateのハンドルとしてfunctionHandleを、Nettyのisolateのためのハンドルとして戻り値をそれぞれ定義します。これは意識的な決断です。

このサンプルの完全な実装は、GitHubリポジトリにあるコメント付きのソースコードをご覧ください。

Isolates for GraalVM Native Images
https://github.com/graalvm/graalvm-demos/tree/master/native-netty-plot

isolateへ入ると、内部レジスタのセットアップと内部スレッド状態をactiveモードに遷移するコードを実行します。isolateを離れると、すべての例外を捕捉、レポートし、内部スレッド状態をinactiveモードに遷移するコードを実行します。plotAsSVG()のコールサイトでは、以下が行われます。

  • 呼び出し前、Nettyのisolateはactiveでレンダリングのisolateはinactive
  • Nettyのisolateを離れる。その結果、両方のisolateはinactiveに遷移
  • レンダリングのisolateに入る
  • レンダリングのisolate内でplotAsSVG()を呼び出す
  • レンダリングのisolateを離れる。その結果、両方のisolateはinactiveに遷移
  • Netty のisolateに入る
  • Nettyのisolateで呼び出し後のコードを実行

おわかりの通り、どのスレッドでも最大で1つのisolateが任意のタイミングでactiveです。下図はスタックを視覚化したものです。2つの異なるリクエストハンドラスレッドで2つのレンダリングisolateがactiveになっているとします。

すべてのスレッドは、スレッド開始ルーチン(Linuxの pthread 関数)用のCのコードで始まります。Nettyのisolateはすべてのリクエストハンドラ・スレッドを開始するので、Nettyのisolateに属するフレームはすべてのリクエスト・ハンドラ・スレッドにあります。最後のフレームはメソッドplotAsSVGInIsolate()用です。各遷移(isolateへの出入り)は遷移フレーム (transition frame) になります。レンダリングisolate内の遷移フレームの後の最初のフレームは、メソッドplotAsSVG()用です。

スタック上でisolateを複数回切り替えることができます。この例では、レンダリングのisolateは結果のByteBufferインスタンスを割り当てるためにNettyのisolateを呼び出します(当該コードはこのエントリでは省略しています)。以下のリストは、プロット結果のためにByteBufferインスタンスを割り当てるメソッドcreateByteBuffer()で停止したときのGDBスタックトレースです。

このスタックトレースで、isolateに入るフレーム(#1、#4と#38)とisolateを離れるフレーム (#5と#2) という、複数のスタックフレームを確認できます(合成関数名で表記)。

Impact of Isolates on Memory Footprint

isolateが及ぼすアプリケーションのメモリ使用量を評価するため、関数を繰り返しプロットするリクエストを送信し、各リクエストの後に常駐メモリセットのサイズを出力します。メモリサイズのクエリにはLinuxコマンドpmap -xprocessidを使います。下図は、50件のリクエストでのisolateの有無の結果を示しています。

最初のデータポイントは、Nettyの起動後、リクエストを処理する前のメモリ使用量で、リクエストごとに約1.8 MBのJavaオブジェクトが割り当てられています。isolateがなければ、これらのオブジェクトはすぐに解放されず、young世代のヒープを埋めていきます。young世代がいっぱいになると、GCを実行してオブジェクトを解放します。このベンチマークでは、young世代のサイズを80 MBに固定していて、リクエスト#39でこの制限に到達します。このリクエストの間のGCの後、young世代は再び空の状態からいっぱいになっていきます。最適化目的でメモリをOSにすぐに返さないため、常駐メモリセットのサイズは大きいままですが、それまでのように直線的に増加することはありません。しかし、約40リクエストごとにGCが必要です。

isolateを使用すると、isolateを削除すればレンダリング中に割り当てられた一時オブジェクトを即座に解放します。このときGCは必要ありません。したがって、常駐メモリセットのサイズは、GCのオーバーヘッドなしに小さく保たれるとともに、GCを使用せずにさらに多くのリクエストを処理できます。

Pre-Initialization of Objects using the Image Heap

各isolateは個々でイメージヒープのコピーをもっています。イメージヒープはビルド時、つまりイメージ生成中に準備されることを忘れないでください。この機能を使用して実行時の初期化コード実行を回避できます。この例では、関数レンダリングのコードはレンダリング目的でSVGGraphics2Dクラスのインスタンスを使用します。従来のJavaの実行では、このオブジェクトをシングルトンにはできず、複数のリクエストを同時に異なるスレッドで処理できます。つまり、複数のインスタンスを同時に使用し、すべてのインスタンスを同じヒープに入れる必要があります。実行時に新しいインスタンスを割り当てる必要があります。

isolateベースのモデルでは、各レンダリングを別々のisolateで実行するため、isolateごとにSVGGraphics2Dのインスタンスが1つだけ存在します。したがってシングルトンインスタンスを持つことができ、イメージ生成中にシングルトンの割り当て、初期化ができます。isolateはイメージ・ヒープ上にすでに存在しているインスタンスで開始され、実行時の割り当てや初期化は不要です。この場合、SVGGraphics2Dは大規模なデータ構造ではないのでメモリ使用量はそれほど節約できませんが、イメージ生成中にはるかに大きなデータ構造を準備する可能性のあるユースケースが多くあります。

簡単にイメージ生成中に事前割り当てするには、クラス初期化子で割り当てを実行し、static finalフィールドを使用します。

しかし、より明示的に事前に初期化をするために、「image singletons」と呼ばれるメカニズムも提供します。

ImageSingletons
https://www.graalvm.org/sdk/javadoc/org/graalvm/nativeimage/ImageSingletons.html

これはイメージ生成中にシングルトンオブジェクトを登録し、実行時にそれらにアクセスできるというものです。初期化は、イメージ生成中に実行される、いわゆる「feature」で行われます。

Interface Feature
https://www.graalvm.org/sdk/javadoc/org/graalvm/nativeimage/Feature.html

--features=com.oracle.svm.nettyplot.PlotterSingletonFeatureオプションを使用して、feature実装クラスの完全修飾クラス名をnative-imageツールに提供する必要があります。

Running the Example

ここでは、以前の記事で紹介したNettyの例をベースにしたサンプルを使います。

Instant Netty Startup using GraalVM Native Image Generation
https://medium.com/graalvm/instant-netty-startup-using-graalvm-native-image-generation-ed6f14ff7692

サンプルの完全なソースコードはGitHubのGraalVM-demosリポジトリのnative-netty-plotフォルダにあります。

Isolates for GraalVM Native Images
https://github.com/graalvm/graalvm-demos/tree/master/native-netty-plot

実行には、GraalVM 1.0 RC9以降のバージョンが必要です。

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

GraalVMにはオープンソースのCommunity Editionと商用のEnterprise Edition(評価目的で無料でダウンロード可能)の2種類があります。isolateは両方のエディションで利用できますが、この記事の後半で紹介されている圧縮参照はEnterprise Editionでのみ利用可能です。したがって、このサンプルの実行にはEnterprise Editionを使用することをお勧めします。

Nettyの以前のエントリ以降、開発者エクスペリエンスを向上し、ビルド手順を大幅に簡素化できました。 前提として、GraalVM 1.0 RC9をホームディレクトリにインストールし、サンプルのリポジトリを複製済みとします。サンプルのnative-netty-plotディレクトリ(pom.xmlファイルの在処)にて、 mvn packageでサンプルをビルドできます。ビルドすると、1個のJARファイルができあがります。target/netty-plot-0.1-jar-with-dependencies.jarの中には、サンプルとそれに紐付く依存関係がすべて含まれています。これでこのアプリケーションのネイティブイメージを生成できます。

$ ~/graalvm-ee-1.0.0-rc9/bin/native-image -jar target/netty-plot-0.1-jar-with-dependencies.jar

native-imageツールは、サブディレクトリMETA-INF/native-image/のJARファイルに包含されているファイルnative-image.propertiesからnative-imageのいくつかのオプションを自動的に取得します。

ようやくWebサーバを実行できるようになりました。

$ ./netty-plot

最後に、ブラウザを開いて以下のURLを指定し、関数の描画をリクエストします。

http://127.0.0.1:8080/?function=abs((x-31.4)sin(x-pi/2))&xmin=0&xmax=31.4

C API for Isolates

JavaのIsolatesクラスと同じ機能がC APIでも利用可能です。これにより、既存のC言語アプリケーションにJavaコードを埋め込むことができます。このシナリオの場合、C言語のコードでisolateのライフサイクルを管理します。つまり、C言語のコードがisolateを作成し、isolate内でJavaメソッド(アノテーション@CEntryPointが付けられたJavaメソッド)を呼び出し、最後にisolateを破棄します。--sharedオプションを指定してnative-imageを実行すると、共有ライブラリを構築してC APIを自動的に公開します。Javaコード用の共有ライブラリに加えて、native-imageツールは型定義と関数プロトタイプを含むCヘッダーファイルを生成します。このヘッダーファイルには、isolateディスクリプタ用にgraal_isolate_t型、スレッドディスクリプタ用にgraal_isolatethread_t型、そしてisolateの作成のためのgraal_create_isolateやisolateの破棄のためのgraal_tear_down_isolateのような関数が含まれています。

GitHubのGraalVMリポジトリには、このAPIと一般的なネイティブイメージのためのC言語のインターフェースの使い方に関するサンプルがたくさんあります。C言語のコード、Javaのコードはそれぞれ以下からアクセスできます。

cinterfacetutorial.c (C言語の例)
https://github.com/oracle/graal/blob/master/substratevm/src/com.oracle.svm.tutorial/native/cinterfacetutorial.c#L96

CInterfaceTutorial.java (Javaの例)
https://github.com/oracle/graal/blob/master/substratevm/src/com.oracle.svm.tutorial/src/com/oracle/svm/tutorial/CInterfaceTutorial.java

Isolate Implementation Details

以下の2つのパフォーマンス上の目標を念頭に置いてisolateを実装しました。

  1. 多数のisolateを短時間で実行するタスク用に作成できるよう、isolateの新規作成は高速かつメモリのオーバーヘッドを低くすること
  2. ピークパフォーマンス、つまりisolate内で実行するコードに対する影響が少ない、もしくは全く影響しないこと

上記目標を達成するために、Javaオブジェクトへの参照の処理方法を変更し、オブジェクトの絶対メモリアドレスを使用するのではなく、イメージヒープの先頭からの相対参照を使用することにしました。これはオブジェクトや配列要素からフィールドをロードする場合のように、メモリアクセスには間接参照が必要であることを意味し、メモリアクセスの前にヒープの開始位置を参照に追加する必要があります。この処理を可能な限り速くするために、ヒープの開始位置は常に固定レジスタで利用可能です(x64アーキテクチャではレジスタr14を使用)。多くの場合、x86アーキテクチャでのメモリアクセス命令に加算を組み込み、明示的な算術演算を避けることができる点に着目してください。

すべてのオブジェクト参照がイメージヒープの開始を基準にしているため、イメージ生成中に準備されるイメージヒープとネイティブ実行可能ファイルの一部は、アドレス空間で複数回メモリマップできます。これにより、コピーせずにイメージヒープを複製(高速なisolateの作成)できるとともに、イメージヒープをcopy-on-writeで共有することでメモリオーバーヘッドを少なくできます。

以下の手順でisolateを新規作成します。

  1. OSのローダーが作成するメモリマッピングを調べて、ディスク上の実行可能ファイル内のイメージヒープを見つける。このステップの結果をキャッシュできるので、以後isolateを作成する際にはこの手順をスキップできる。
  2. ディスクから、先ほど予約したメモリ範囲の先頭へイメージヒープの読み取り専用マッピングを作成する。
  3. copy-on-writeとして書き込み可能オブジェクトを含むマッピングのパーティションをマークする。
  4. 指定されたヒープベース・レジスタ(x64アーキテクチャではr14)を、予約されたメモリ範囲の開始アドレスを含むように設定する。このメモリアドレスの先頭に、イメージヒープのマッピングがある。
  5. 現在のスレッドをisolateにアタッチし、スレッド固有の実行コンテキストを作成し、アタッチされたスレッドのisolateごとのリストにそのスレッドを追加する。

isolateの作成時に、イメージ・ヒープ内のJavaオブジェクト間の参照は再配置を必要としないことに注意してください。これらの参照は、Javaオブジェクト間の他の参照と同様に、ヒープ開始位置からの相対位置であるためです。

isolateは新しいスレッドを開始することも、既存のスレッドをisolateにアタッチすることもできます。スレッドとisolateの間にはn:mの関係にあって、1つのスレッドを複数のisolateに接続でき、1つのisolateに複数のスレッドを接続できます。

後でisolateを破棄するために、isolateの残存するすべてのスレッドが中断され、その結果、クリーンなシャットダウンのために処理して渡すことが可能な例外が各スレッドで発生します。すべてのスレッドが終了すると、isolateはそのメモリ範囲全体をOSに返すことによって破棄されます。

Compressed References

クラウドでソフトウェアを実行する際の費用に直接影響するため、メモリ使用量は重要です。isolateのためのイメージヒープのcopy-on-write共有により、いくらかのメモリフットプリントの改善が可能ですが、特にJavaのような参照の多いマネージド言語では、各参照のための64ビットが積み重なって、かなりのオーバーヘッドになります。控えめなヒープサイズで実行している限り、32ビットで参照可能な最大ヒープサイズは32 GByte (2³⁵ Byte) なので、参照には32ビットで十分です。32ビットの参照に対する追加の3ビットのアドレス範囲は、通常の8バイトのオブジェクトアライメントによるもので、参照の最下位3ビットを記憶する必要はありません。

参照の圧縮および圧縮解除の操作は、多くの場合x86メモリアドレッシングモードに組み込むことができます。Java HotSpot VMは昔から圧縮参照をサポートしているので、その技法はよく理解されています。しかし、Java HotSpot VMは初期イメージヒープをサポートしていないため、圧縮参照のベースとして定数を使用できます。isolateの導入によってのみ、ネイティブイメージでの圧縮参照をサポートします。すべてのメモリアクセスはすでにイメージヒープの先頭(レジスタr14に格納されている)に対して相対的であるため、完全な64ビット参照ではなく、ヒープの先頭からの相対参照に対して32ビット参照のみを使用することは簡単です。

圧縮参照の実装は、新しいisolateを作成するために上記のステップ1を少し変更するだけで済みます。OSからイメージヒープだけを保持するのに十分な大きさのメモリ範囲を予約するのではなく、最大ヒープサイズをカバーするアドレス範囲を予約します。デフォルトでは、32 GBのアドレス空間をすべて予約します。このメモリを、物理メモリが支えることを要求しません。割り当てられたオブジェクトが実際に占有するアドレス空間の部分だけがコミットされます。

Javaオブジェクトのメモリは、予約済みの連続したメモリ範囲に割り当てる必要があります。そのためには、自身でその範囲を管理し、物理メモリが部分範囲を補助するというOSからのリクエストだけでなく、物理メモリが補助している未使用の部分範囲のOSへの返却も管理する必要があります。

このサンプルでは、圧縮参照を使ってどれだけのメモリを節約できるでしょうか。リクエスト・ハンドラは、isolate作成後および破棄前に、レンダリングのisolateのJavaヒープサイズを出力します。圧縮参照の場合の出力は以下のようになりました。

Rendering isolate initial memory usage: 4114 KByte
Rendering isolate final memory usage: 5910 KByte

初期メモリサイズはイメージヒープのサイズですが、このメモリはcopy-on-writeであることを思い出してください。そのため、OSがコミットすべき部分はごく一部です。初期メモリサイズと破棄前のメモリサイズの差である1795 KByteがレンダリング中に割り当てられたメモリです。

以下は圧縮参照を使わなかった場合の結果です。

Rendering isolate initial memory usage: 4895 KByte
Rendering isolate final memory usage: 6936 KByte

イメージヒープのサイズが大きくなっており、レンダリング中に割り当てられたメモリの量が2041 KByteに増加しています。

Caveats

isolateと圧縮参照はSubstrate VMの新機能です。お試しいただいて是非フィードバックをお寄せください。圧縮参照はデフォルトで有効ですが、-H:-UseCompressedReferencesオプションを使って無効化できます。

isolateはLinux、macOSの両方でサポートされますが、macOSの実装は最適化が進んでいません。新しいisolateを作成すると、Linuxではイメージファイルからマッピングするところ、macOSでは常にイメージヒープをコピーするために少し時間がかかります。macOSでも完全にサポートするように取り組んでいる最中ですので、ご期待ください。

Summary

このエントリでは、GraalVMネイティブイメージの2つの高度な機能(isolate機能によるより柔軟なメモリ管理、および圧縮参照によるネイティブイメージのメモリ使用量の削減)について説明しました。どちらもGraalVMの最新のリリースで利用可能になっているので、これらを試してみたい場合は、Webサイトからバイナリを入手して試してみてください。これらは高度な機能なので、以前にネイティブイメージを作成したことがない場合は、その便益がよくわからない可能性があります。しかし、アプリケーションのメモリの一部を保護したり、全体として解放可能な多くのオブジェクトを生成する計算を実装したり、あるいは外部の制限に基づいてアプリケーションのメモリを個々のチャンクに分割したりしたい場合は、isolateがお役に立ちます。圧縮参照を使用すると、コストをかけずにメモリ使用量を減らすことができます。これは、メモリ使用量が非常に重要なプラットフォームでは特に魅力的です。