GraalVMは「新しい仮想マシン」と紹介されがちですが、実体はNative Imageというコンパイラを同梱したJDKの配布物です。javacとjavaが入っていて、IDEのJDK設定にもそのまま指定できます。この記事では、通常のJDKとの違い、2025年に切り替わったバージョン体系、Oracle版とCommunity Editionの機能差を整理したうえで、両エディションの25.4で同じプログラムをネイティブ化した実測値と、JavaからJavaScriptを呼ぶPolyglot APIの挙動まで確かめます。
まとめ
GraalVMはOpenJDKをベースにしたJDK配布物で、Java用のJITコンパイラ「Graal」と、Javaコードを単体の実行ファイルへ事前コンパイルする「Native Image」を含みます。2026年10月18日時点の最新はGraalVM 25.4.4.1.1(2026年9月22日公開)で、次の25.5は10月27日に予定されています。版の数え方は「GraalVM for JDK 24」までのJDK追従型から、25.0(LTS)と月次の25.x(Innovation)へ変わりました。
エディションの差は実測でも表に出ました。Community Edition(CE)では--pgo-instrumentと--gc=G1がビルドエラーになり、Oracle GraalVMではPGOビルドの実行ファイルが既定ビルドより約1割速く動きました。今回のHello Worldでは、プロセス終了までの実時間と最大RSSの中央値はネイティブ版が0.02秒・約7MiB、JVM版が0.07〜0.14秒・約40〜85MiBでした。一方、今回のHashMap集計を300周させた測定では、CEのネイティブ版とJVM版の実行時間帯が重なりました。起動時間の短縮だけが目的なら、Native Imageより先にJDK 25のAOTキャッシュを試すのが順当です。
Native Imageを内蔵したJDK配布物としてのGraalVMの構成
GraalVMはOracleが配布しているJDKディストリビューションで、Amazon CorrettoやEclipse Temurinと同じくOpenJDKのソースからビルドされています。java -versionもクラスパスの扱いも既存のJDKと変わりません。名称に「VM」が付きますが、HotSpot VMを置き換える独自の仮想マシンではありません。
Graal JITコンパイラとNative Imageという2本の柱
GraalVMが標準のJDKに足しているものは2つです。ひとつはJavaで書かれたJITコンパイラ「Graal」で、実行中のプロファイルをもとにバイトコードを機械語へ変換します。GraalVMを普通のJDKとして使うと、HotSpotのC2の代わりにこのGraal JITが既定で動きます。もうひとつがNative Imageで、実行前にアプリケーション全体を静的解析し、到達可能なクラス・メソッド・リソースだけを含む単体バイナリを生成します。JITとAOTの原理的な違いはJITコンパイラとは?仕組み・AOTとインタプリタとの違いと主要言語の実装で整理しています。
Javaのリリーストレインから外れた2025年9月の方針転換
Oracleは2025年9月、ブログ「Detaching GraalVM from the Java Ecosystem Train」でGraalVMをJava SEのリリース周期から切り離すと発表しました。Oracle Java SE Support Roadmapには「GraalVM for JDK 24 was the final version of GraalVM licensed and supported as part of Oracle Java SE Products」と明記されており、Java SEのサブスクリプションでGraalVMのサポートを受ける形はJDK 24で終わっています。起動時間とフットプリントの改善はOpenJDKのProject Leydenで続け、GraalVMチームはGraalPyやGraalJSなどJava以外の言語へ軸足を移す、というのが発表の骨子です。
実務への影響は2つあります。GraalVMの版番号がJDKの版番号と一致しなくなったこと(次章)と、起動高速化の選択肢としてJDK標準のAOTキャッシュが育ってきたことです(後半の章)。
通常のJDK(HotSpot)との違いと使い分けの判断軸
「GraalVMと普通のJVMは何が違うのか」は、実行エンジンの選択肢の違いに行き着きます。仮想マシンそのものの役割はJava仮想マシン(JVM)とは?仕組み・メモリ構成・JITを実機の出力で解説が前提知識になります。
OpenJDKからGraal JITが外れJVMCIだけが残った経緯
Graalコンパイラは、かつてOpenJDKにも実験的機能として入っていました。JDK 9でjaotcによるAOTコンパイル(JEP 295)、JDK 10でGraalの実験的JIT(JEP 317)が加わっています。しかしJEP 410により、JDK 17でjdk.aotモジュールとGraal JITはOpenJDKから削除されました。導入以降ほとんど使われず、保守コストが見合わなかったことが理由です。
同じJEPは、外部でビルドしたコンパイラを差し込めるようJVMCI(Java-Level JVM Compiler Interface)を実験的機能として残すとしています。GraalVMのGraal JITはこのJVMCI経由で動く外部コンパイラなので、標準のOpenJDKを入れてもGraal JITは付いてきません。
起動速度とピーク性能が入れ替わるトレードオフ
Native Imageの実行ファイルはHotSpot VMではなく、GraalVMが提供する軽量なランタイムの上で動きます。JITは動かないため、実行時プロファイルにもとづく最適化は行われません。公式のPGOドキュメントも、AOTコンパイラはコードの静的な姿しか見えず、if文の各分岐を等しく起こりうるものとして扱うため、JITと同等の機械語を出すのは難しいと説明しています。
判断軸はここで割り切れます。プロセスが短命で起動時間が支配的なCLIツール、サーバーレス関数、頻繁にスケールアウトするコンテナならNative Imageが効きます。長時間稼働するサービスでは、起動時の差に加え、実際の負荷でのスループットとメモリ使用量を比較して判断します。
25.0 LTSと25.4 Innovationのバージョン体系と入手経路
2023年から2025年前半までのGraalVMは「GraalVM for JDK 21」「GraalVM for JDK 24」のようにJDKの版へ追従した名前を使っていました。公式リリースカレンダーは、2025年9月より前はこの表記で、以降は「GraalVM 25」という形式に変わったと説明しています。
LTSの25.0とInnovationの25.xの更新周期
25.1以降、Oracle GraalVMとCEはどちらもInnovation(フィーチャー)リリースを月次で出し、最新の1本が過去のフィーチャーリリースをすべて置き換えます。25.4のリリースノートは、CEを「OpenJDK 25.0.4.1ベースのinnovation release」と説明しています。版番号の後半はベースJDKの番号で、CEの配布ファイル名もgraalvm-community-jdk-25i4-25.0.4.1.1_linux-x64_bin.tar.gzのように「25i4」というInnovation番号を含みます。
| 系統 | 位置づけ | Oracle GraalVMの最新 | CEの最新 |
|---|---|---|---|
| GraalVM 25.4 | Innovation | 25.4.4.1.1(2026-09-22) | 25.4.4.1.1(2026-09-22) |
| GraalVM 25.0 | LTS | 25.0.4.1.1(2026-09-15) | 25.0.2(2026-01-20) |
| GraalVM for JDK 21 | LTS | 21.0.12.1.1(2026-09-15) | 21.0.2(2024-01-16) |
| GraalVM for JDK 17 | LTS | 17.0.20.1.1(2026-09-15) | 17.0.9(2023-10-24) |
右2列の差が実務上の落とし穴です。カレンダーは四半期ごとのCPU(Critical Patch Update)を「Oracle GraalVM向けに1月・4月・7月・10月の第3火曜に公開する」と定義しており、実際にCPU単独の回と、毎月の緊急修正であるCSPUの回は、CE列が空欄です。2日後の2026年10月20日のCPU(Oracle GraalVM 25.0.5ほか)もCE列は空欄で、CEの更新は翌週10月27日の25.5で届きます。CEでLTSに固定すると、25.0系は25.0.2、JDK 21系は21.0.2で更新が止まります。CPUとCSPUの関係はOracle CSPUとは|Javaの月次パッチとCPUの違い・版番号の判定が詳しく扱っています。
選び方は二択です。LTSに固定して四半期のセキュリティ更新を受けたいならOracle GraalVM 25.0、無償のCEで運用するならInnovationを毎月追う。Innovationのある版を長期固定する使い方は、どちらのエディションでも想定されていません。
SDKMAN!・コンテナ・GitHub Actionsでの導入コマンド
公式ダウンロードページのコンテナタグは25i4のようなInnovation番号の形式で、native-image:25.4と書くと「not found」で取得に失敗します。Native Imageあり/なしでイメージが分かれ、CEは配布元も別です。
# Oracle GraalVM(GFTC)
docker pull container-registry.oracle.com/graalvm/native-image:25i4
docker pull container-registry.oracle.com/graalvm/jdk:25i4
# Community Edition(GPLv2+Classpath例外)はGitHub Container Registry
docker pull ghcr.io/graalvm/native-image-community:25i4
docker pull ghcr.io/graalvm/jdk-community:25i4
# SDKMAN!(Linux x64の候補ID。接尾辞graal=Oracle、graalce=CE)
sdk install java 25.4.4+1-graal
sdk install java 25.4.4.1+1-graalce
SDKMAN!の候補IDはエディションで版の書き方が違い、Oracle版は25.4.4+1、CEは25.4.4.1+1です。ローカルでJDKを切り替える手順はSDKMAN!の使い方|Javaのインストール・切り替えと.sdkmanrc、Windows・macOSの注意点で扱っています。
Intel Mac(macOS x64)は対象外になりました。GraalVM 25のリリースノートは、25.0.2でmacOS x64のサポートを削除し、25.0.1が最後の対応版だと明記しています。実際、SDKMAN!のmacOS x64向け一覧ではOracle版・CEとも25.0.1が最新で、CEのGitHub Releasesもjdk-25.0.2以降はLinux x64/AArch64、macOS AArch64、Windows x64の4種だけです。Intel MacではLinuxコンテナ経由で使うのが現実的で、この記事の実測もその構成です。
CIで使うgraalvm/setup-graalvmは、distributionの既定値がgraalvm=Oracle GraalVMです。CEを使うなら明示的に切り替えます。
- uses: graalvm/setup-graalvm@v1
with:
java-version: '25'
version: '25.4' # LTSなら '25.0'
distribution: 'graalvm' # Oracle GraalVM
# distribution: 'graalvm-community' # Community Edition
github-token: ${{ secrets.GITHUB_TOKEN }}
Oracle GraalVMとCommunity Editionで使える機能とライセンスの差
どちらのエディションでもNative Imageは使えますが、最適化機能に差があります。ここを見落とすと「ドキュメントどおりのオプションが通らない」事故になります。
| 項目 | Oracle GraalVM | Community Edition |
|---|---|---|
| ライセンス | GFTC | GPLv2+Classpath例外 |
| 商用・本番利用 | 無償で可 | 無償で可 |
| 有償での再配布 | 不可 | GPLv2の条件下で可 |
| PGO | 可 | 不可 |
| G1 GC(Native Image) | Linux AMD64/AArch64のみ | 不可 |
| 既定の最適化(-O2) | ML推論の静的プロファイル付き | 静的プロファイルなし |
| -O3 | GraalNNで推論(PGO時は推論なしで-O3) | -O2と同一 |
| LTS系統の四半期CPU | 提供あり | 提供なし |
CEで--pgo-instrumentと--gc=G1が拒否される実際のエラー
公式のメモリ管理ドキュメントは、Native ImageのGCはSerial GCが既定で、G1 GCはLinux AMD64/AArch64のOracle GraalVMでのみ使えるとしています。CE 25.4で実際に指定すると、次のエラーで止まりました。PGOのオプションはそもそも存在しない扱いで、オプションではなくメインクラス名と解釈されます。
$ native-image --gc=G1 -cp out App
Error: Invalid option '--gc'. 'G1' is not an accepted value. Accepted values are 'epsilon', 'serial'.
$ native-image --pgo-instrument -cp out App
Error: '--pgo-instrument' is not a valid mainclass. Specify a valid classname for the class that contains the main method.
Oracle GraalVM 25.4では同じ2つが通り、G1版はGarbage collector: G1 GC (max heap size: 25.0% of RAM)と表示されて約11.9MiBの実行ファイルが出ました。既定のSerial GCでは最大ヒープがRAMの80%なので、GCを切り替えるとヒープ上限の既定値も変わる点に注意が要ります。
最適化レベルにも差があります。最適化ドキュメントによれば、Oracle GraalVMの既定-O2はGraal Static Profiler(GraalSP)で分岐の確率を推論し、-O3ではニューラルネットのGraalNNを使います。どちらもCEにはなく、「CEでは-O3と-O2は同一」「Oracle GraalVMの-O1がCEの-O2にほぼ相当」とまで書かれています。実測のビルドログでも、Oracle版の既定ビルドにはPGO: ML-inferredと出ました。JVM側のGC選定と混同しやすいので、Javaのガベージコレクション(GC)とは|仕組み・種類・ログ確認とチューニング手順で扱うHotSpotのGCとは別系統だと押さえておいてください。
GFTCとGPLv2の違いとJDK 17系のOTN切り替え
Oracle GraalVMのGFTCは商用・本番利用を許可し、再配布も認めていますが、配布に関して料金を取ることは禁じています。ライセンス本文は、プログラムを同梱した製品の料金や、配布したプログラムを使うサービスの料金もこれに含め、Native Imageの出力に含まれるプログラム部分も対象としています。注意が要るのはJDK 17系で、GraalVM for JDK 17.0.13以降のCPUはGraalVM OTNライセンスに変わりました。ダウンロードページはJDK 21と25.0・25.4がGFTCのまま提供されると記載しています。
Native Imageでネイティブ実行ファイルを作る手順
native-imageはGraalVMのbinディレクトリに同梱されていて、追加インストールは不要です。ただし最終段でシステムのリンカを使うため、Cのツールチェーンが要ります(Ubuntuはbuild-essential zlib1g-dev、Oracle Linuxはgcc glibc-devel zlib-devel、macOSはxcode-select --install、WindowsはMSVC)。
native-imageコマンド単体とMavenプラグインの実行手順
最小構成なら、mainメソッドを持つApp.javaを用意し、次のコマンドでコンパイルしてNative Imageへ渡します。JARを対象にするならnative-image -jar app.jarの形式を使います。
javac -d out App.java
native-image -cp out App -o app
./app
実プロジェクトではビルドツールのプラグイン経由が現実的です。Mavenではnative-maven-pluginをnativeプロファイルに置き、compile-no-forkゴールをpackageフェーズへ紐づけます。版は<properties>でnative.maven.plugin.versionとして定義し、JARのマニフェストにメインクラスが無い構成では<mainClass>で指定します。
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native.maven.plugin.version}</version>
<extensions>true</extensions>
<configuration>
<mainClass>com.example.App</mainClass>
</configuration>
<executions>
<execution>
<id>build-native</id>
<goals><goal>compile-no-fork</goal></goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
mvn -Pnative packageで実行ファイルがtarget/に出て、<extensions>true</extensions>を付けているので、mvn -Pnative testでテストもネイティブで実行されます(Maven Surefire 3.0.0以上とJUnit Platformが前提)。Gradleはorg.graalvm.buildtools.nativeプラグインを適用して./gradlew nativeCompileです。native-build-toolsは更新が速く、Maven Centralでは2026年8月から9月にかけて1.1.10から1.1.14まで5版が出ています(1.1.14は9月18日)。版はプロパティに外出しし、GraalVM本体を上げるときにまとめて上げるのが無難です。Spring Bootで使う場合は、Spring Bootのシステム要件が「GraalVM 25以上」とNative Build Tools 1.1.8を挙げているので、Spring Bootバージョン一覧とサポート期限(EOL)【2026年10月】最新版は4.1、4.0の無償サポートは12月末終了のサポート表と突き合わせてください。
リフレクションを通すreachability metadataの用意
ビルドは通るのに実行時にClassNotFoundExceptionが出る、というのがNative Imageの典型的な詰まり方です。実測用のプログラムで、実行時に決まるクラス名をClass.forName(name)で読み込むと、JVMでは動くのにネイティブ版は次の例外で落ちました(CE・Oracleとも同じ)。
$ ./app reflect Plugin
Exception in thread "main" java.lang.ClassNotFoundException: Plugin
at org.graalvm.nativeimage.builder/com.oracle.svm.core.hub.registry.ClassRegistries.forName(ClassRegistries.java:243)
リフレクション、JNI、動的プロキシ、クラスパス上のリソースなどは実行時のデータに依存するため、ビルド時の静的解析では到達可能か判定しきれません。そこでreachability metadataとして明示的に教えます。登録対象が多い場合は、通常のJVMでアプリを動かして記録するTracing Agentを使うと、手書きの負担を減らせます。
# JVMで実行しながら必要なメタデータをJSONに書き出す(-agentlibは-jarやクラス名より前に置く)
java -agentlib:native-image-agent=config-output-dir=cfg -cp out App reflect Plugin
# 書き出されたcfg/reachability-metadata.json
{
"reflection": [
{ "type": "Plugin", "methods": [ { "name": "<init>", "parameterTypes": [] } ] }
]
}
# 設定ディレクトリを渡して再ビルドすると "Plugin loaded" が表示された
native-image -H:ConfigurationFileDirectories=cfg -cp out App -o app_cfg
実プロジェクトでは、このJSONをMETA-INF/native-image/(groupId)/(artifactId)/配下に置けば自動で読み込まれます。エージェントは実行した経路しか記録しないため、分岐の異なるシナリオをconfig-merge-dir=で複数回流してマージするのが前提です。登録漏れを早く見つけたいなら、ビルド時に--exact-reachability-metadataで厳格モードにするか、実行時に-XX:MissingRegistrationReportingMode=Warnで漏れ箇所を警告として一覧できます。
25.2〜25.4で変わった既定値と最適化
Innovationが月次になったぶん、既定値の変更も細かく入ります。25.0から上げるときに確認したい変更は次のとおりです。
- 25.2:圧縮32ビットオブジェクト参照が既定で有効になり、ヒープが32GBに制限されます。それを超えるなら
-H:-UseCompressedReferencesで無効化します。-H:-SpawnIsolatesはビルドエラーになりました。 - 25.3:Graal JITの既定のインライン化アルゴリズムがpriority inliningに変わり、配列演算ループの自動ベクトル化も既定で有効になりました(戻すなら
-Djdk.graal.UsePriorityInlining=false、-Djdk.graal.VectorizeLoops=false)。 - 25.4:Serial GCではidentity hash codeのフィールドを必要なオブジェクトにだけ後付けする方式が既定になり、
StringBuilderの連結処理のアウトライン化で実行ファイルが小さくなりました。
いずれも何も指定していないビルドほど挙動が変わります。性能の回帰を疑うときは、まずリリースノートに書かれた無効化オプションで切り分けてください。
CEとOracle GraalVM 25.4で同じプログラムをネイティブ化した実測
CEとOracle GraalVMの差、JVMとの差を数字で確かめるため、同じJavaプログラムを両エディションの25.4(公式コンテナnative-image-community:25i4とnative-image:25i4)でビルドし、実行しました。環境はIntel Core i9-9880HのMac上のLinux VM(x86_64・4 vCPU・6GiB)で、ホストでは他の処理も動いていたため、同条件の繰り返しでも振れがあり、CEのJVMで300周させた3回は830〜1,828msと2倍以上開きました。表の「起動」はHello Worldプロセスの開始から終了までの実時間、併記したメモリは実行中の最大RSSで、いずれも11回実行の中央値です。サービスの応答開始時間や定常稼働時のメモリ使用量は測っていません。
| 項目 | CE 25.4 | Oracle GraalVM 25.4 |
|---|---|---|
| ビルド時間(既定 -O2) | 1分22秒 | 3分43秒 |
| ビルド時間(-Ob) | 55秒 | 1分49秒 |
| ビルド時のピークメモリ | 1.05GiB | 1.29GiB |
| 実行ファイル(既定) | 10.25MiB | 12.32MiB |
| 実行ファイル(PGO) | 不可 | 8.63MiB |
| 起動(ネイティブ) | 0.02秒・7.0MiB | 0.02秒・7.2MiB |
| 起動(同梱JVMで実行) | 0.13秒・84MiB | 0.14秒・85MiB |
| 短い計算(ネイティブ/JVM) | 22〜23ms/109〜131ms | 19〜25ms/146〜174ms |
| 長い計算(ネイティブ/JVM) | 901〜1,135ms/830〜1,828ms | 847〜927ms/989〜1,086ms |
| 長い計算(PGO) | 不可 | 741〜800ms |
「短い計算」はHashMapへの集計を5周、「長い計算」は300周させた処理で、プログラム内で計った経過時間で、表は各条件3回の最小〜最大です(JVMの起動時間は含みません)。読み取れることは3つあります。
第一に、差が大きく出たのは5周の短い処理です。3回の中央値で、CEはネイティブ22ms対JVM123ms、Oracle版は19ms対148msでした。300周では、CEのネイティブとJVMの範囲が重なり、Oracle版でもネイティブの優位は1割強に縮みました。第二に、Oracle GraalVMのPGOは効きます。300周の処理を計測用ビルドで1回流して作ったプロファイルで再ビルドすると、ログにoptimization level: 3 ... PGO: user-providedと出て、既定ビルドより約1割速く、実行ファイルも30%小さくなりました。第三に、Oracle版の既定ビルドはCEより2倍以上時間がかかりました。ビルド時間差の原因はこの測定だけでは分離できません。ビルド待ちが開発を妨げる場合は、-Obで短縮を試せます。
もうひとつの発見は、GraalVMを普通のJDKとして使ったときの最大RSS(実行中に使った物理メモリの最大値)です。同じHello Worldのjava -jarは、Temurin 25.0.4.1で約40MiBだったのに対し、GraalVM 25.4では約84MiBでした。Graal JITを外す-XX:-UseJVMCICompilerを付けて同じく11回実行すると、中央値は約44MiBまで下がりました。差の大半はGraal JITを有効にしていることに伴う分ですが、その内訳(共有ライブラリ、コンパイラの作業領域など)までは、この測定では分けられません。GraalVMをJVMとして使う場合も、コンテナのメモリ上限に収まるかを実際のアプリケーションで測定してください。
TruffleとPolyglot APIでJavaScriptやPythonを呼ぶ方法
GraalVMは、かつてJavaScript・Python・Rubyなどを同梱した「多言語プラットフォーム」として紹介されていました。現在のJDK本体には他言語のランタイムは入っておらず、CEのGitHubリリースページもPythonやJavaScript/Node.jsのランタイムは関連リポジトリのスタンドアロン配布として提供すると明記しています。Javaアプリに組み込むなら、Maven依存として足すのが標準の経路です。
Truffleが言語実装を速くする仕組み
GraalJSやGraalPyは、Truffleという言語実装フレームワークの上に作られています。Truffleでは言語のインタプリタを抽象構文木(AST)のノードとしてJavaで書き、ノードは実行中に観測した型に合わせて自分自身を特化した実装へ書き換えます。整数どうしの加算しか来ない箇所はlong専用のノードに固定され、文字列が来た時点で汎用のノードへ戻る、という具合です。関数がホットになると、GraalコンパイラはそのASTを定数とみなしてインタプリタごと畳み込み(部分評価)、その関数専用の機械語を生成します。この最後の段がGraalコンパイラを前提にしている点が、次の実測につながります。なお、検索で混ざるEthereum開発ツールのTruffle Suiteは名前が同じだけの別製品で、2023年にサンセットが発表済みです。
ContextとValueとMaven依存で組み込む手順
Javaから他言語を呼ぶだけなら、TruffleのAPIは触りません。org.graalvm.polyglotのContextを開き、evalの戻り値Valueを通して操作します。依存は公式の埋め込みガイドどおり、API本体と言語を並べます。言語側には<type>pom</type>が必須で、言語の成果物は既定でGFTCライセンスのため、CE相当のライセンスにしたいときはjs-communityのように-communityを付けます。
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>polyglot</artifactId>
<version>25.4.4.1.1</version>
</dependency>
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>js-community</artifactId>
<version>25.4.4.1.1</version>
<type>pom</type>
</dependency>
try (Context context = Context.create("js")) {
Value fib = context.eval("js",
"(function fib(n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); })");
System.out.println(fib.execute(27).asInt()); // 196418
}
この2つの依存だけで、truffle-apiやjs-language、regex、icu4jなど12個のJARが解決されました。Contextは既定でホスト側のJavaクラスへのアクセスを遮断しているので、Javaのオブジェクトを渡したいときはContext.newBuilder()で許可範囲を明示します。この例は信頼できるスクリプト向けです。信頼できないコードを実行する場合は、ホストアクセスの制限に加え、公式のSandboxPolicyと資源制限・隔離方式を確認してください。
OpenJDK 25でのフォールバック動作と最適化ランタイムの選択
依存をMavenで足せるので「GraalVMを入れなくても普通のJDKでGraalJSが動く」と説明されることがあります。動くのは事実ですが、速度は別物です。上のfib(27)を10回続けて実行したところ、Temurin 25.0.4.1では起動時に次の警告が出て、10回目でも79msかかりました。同じJARをGraalVM CE 25.4で動かすと、10回目は2msでした。
[engine] WARNING: The polyglot engine uses a fallback runtime that does not support runtime compilation to native code.
Execution without runtime compilation will negatively impact the guest application performance.
The following cause was found: JVMCI is not enabled for this JVM. Enable JVMCI using -XX:+EnableJVMCI.
フォールバックランタイムとは、ゲスト言語のコードを機械語へ実行時コンパイルせず、インタプリタだけで動かす状態です。公式の対応表は、Oracle GraalVM 25とCE 25では最適化ランタイムが追加設定なしで使える一方、Oracle JDK 25/OpenJDK 25とJDK 21系では「Polyglot isolate only」としています。isolateではゲスト言語がネイティブイメージとして隔離環境で動き、依存をjs-isolate(Oracle GraalVM)やjs-isolate-community(CE 25.1以降)に替えたうえで、Context.newBuilder("js").spawnIsolate(true).build()のように明示的に有効化します。設定ファイルの式評価のように呼び出しが少ない用途ならフォールバックのままで構いませんが、スクリプトの実行時間が処理の大半を占めるなら、実行基盤ごとGraalVMに揃えてください。
Native Imageを避けるべき条件とJDK 25のAOTキャッシュという代替
起動時間・メモリ使用量・配布形態に改善要件がなく、実負荷での性能改善も確認できないなら、Native Imageの導入は見送るべきです。今回の実測でも、大きな差が出たのは起動からの短い処理と最大RSSで、300周の計算では差が縮みました。
避けた方がよい条件は具体的です。第一に、リフレクションやバイトコード生成を多用するライブラリ(古いORM、DIコンテナ、一部のシリアライザ)を抱えている場合で、reachability metadataの収集と維持が恒常的な運用コストになります。第二に、長時間稼働して単位時間あたりの処理量が指標になる場合で、今回の300周の計測でもPGOなしのネイティブ版とJVMの差は小さく(ウォームアップを分けた定常性能の比較ではありません)、PGOを使うにはOracle GraalVMへ寄せる判断が要ります。第三に、ビルドの重さです。実測では小さなプログラムでもピークメモリ1GiB超、Oracle版で約4分かかりました。Javaの業務システムをゼロから設計する段階なら、こうした実行基盤の選定まで含めてフルスクラッチ開発の要件に入れておくと手戻りを防げます。
起動を速くする手段は、Native Imageだけではありません。Project LeydenのJEP 483がJDK 24でAOTキャッシュを導入し(経緯はJava 24の新機能と主要JEPまとめ|非LTS版の位置づけとJava 25 LTSへの移行判断)、JDK 25ではJEP 514によりトレーニング実行とキャッシュ生成が1コマンドになりました。
# JDK 25:トレーニング実行とキャッシュ生成を1コマンドで
java -XX:AOTCacheOutput=app.aot -jar app.jar
# 本番実行はキャッシュを指定するだけ
java -XX:AOTCache=app.aot -jar app.jar
キャッシュの対象はJARで、クラスパスにクラスファイルのディレクトリを渡すとError: non-empty directory 'out-temurin'と出てapp.aotは作られませんでした。効果はクラスのロード・リンク等が実行時間に占める割合や、処理内容によって変わります。Temurin 25.0.4.1で、1クラスだけのHello Worldは0.07秒のまま変わりませんでした。HTTPクライアント・日時・正規表現・仮想スレッドなどを使って2,207クラスを読み込むプログラムでは、プロセス終了までの時間が1.03秒から0.75秒へ約27%短くなり(15回の中央値)、キャッシュは21.2MiBでした。HotSpot上で動くのでJITもリフレクションもそのまま使え、JARで起動する構成なら、アプリケーションのコードに手を入れずに済むのが決定的な利点です。キャッシュは作成時と同じJDKとクラスパスが前提なので、JDKやJARを更新したら作り直します。起動時間が目的なら、まずAOTキャッシュを試し、それでも足りない場合や、メモリ削減・単体バイナリでの配布が要件になる場合にNative Imageへ進むのが、労力に見合った進め方です。JDK 21から25へ上げる際の注意点はJava 21と25の違い|正式化した新機能・サポート期限・移行で止まる箇所を実機で比較にまとめています。
よくある質問
GraalVMの読み方は?
公式サイトで日本語の読みを定めた記述は確認できませんでした。Native Imageのリフレクションを扱うOracleの日本語チュートリアルでも、表記は英字の「GraalVM」のままでカタカナ表記はありません。会話では「グラールVM」と読まれることが多いものの、ドキュメント参照や検索の互換性を考えると、文章中は英字表記で統一しておくのが確実です。
GraalVMは無料で商用利用できますか?
できます。Oracle GraalVMはGFTCのもとで商用・本番利用が無償です。SaaS提供についても、公式FAQはプログラムのコピーを受け取らない第三者に使わせるホスティング利用を「自社の内部業務のための利用」とみなし、そのサービスに料金を課してよいとしています。一方、Oracle GraalVMで作った実行ファイルを顧客へ配布する場合は、その配布物や同梱した製品、配布物を使うサービスに料金を課せません。CEはGPLv2+Classpath例外なので、有償配布も同ライセンスの条件下で可能です。
GraalVMとOpenJDKはどちらを使うべきですか?
Native ImageもGraal JITも使う予定がないなら、通常のOpenJDK系ディストリビューションで十分です。JDK 17以降のOpenJDKにはGraal JITが含まれず、GraalVMを入れる理由はこの2つとPolyglot APIの最適化ランタイムに絞られます。今回のHello WorldではGraalVMの最大RSSがTemurinの約2倍でしたが、この比率を実サービスの定常メモリ使用量には一般化できません。
GraalVMでPythonやJavaScriptを動かせますか?
動かせますが、JDK本体には入っていません。JavaScriptはorg.graalvm.polyglot:js(CE相当はjs-community)、Pythonはpython(同python-community)をMaven依存に足し、Polyglot APIで呼び出します。版はGraalVM本体と揃え、25.4系なら依存も25.4.4.1.1です。リリースカレンダーでは、25.4のGraalPyはPython 3.13系、10月27日予定の25.5では3.14系と記載されています。
Native Imageのビルドが遅いときの対処法は?
開発中は-Ob(quick build mode)を使います。実測ではCEで1分22秒が55秒に、Oracle GraalVMで3分43秒が1分49秒に縮みました。実行ファイルのサイズはOracle版では小さくなった一方、CEではわずかに大きくなったので、配布物は既定の-O2で作り直してください。CIでは通常ビルドとネイティブビルドを分け、後者は夜間や依存ライブラリの更新時に定期実行すると、メタデータ不足をリリース直前まで持ち越さずに済みます。