GraalVM on Ubuntu — JVMアプリをネイティブバイナリにコンパイル

開発環境

Java アプリをそのまま動かすのではなく、起動時間を 1ms 以下のネイティブバイナリ にコンパイルしたい。GraalVM の native-image を Ubuntu に入れると、それが実現できます。

私が ghcr.io/graalvm/graalvm-community:21 コンテナで実際に試したところ、JVM 実行の平均起動時間 195ms が、ネイティブバイナリでは 1ms に短縮されました(10 回計測の平均)。この記事では Ubuntu 24.04 LTS に GraalVM CE 21 をインストールし、Java ソースをネイティブバイナリにコンパイルするまでの手順を実測ログとともに紹介します。

この記事のポイント

  • GraalVM CE 21 は無償。native-image が同梱されており追加インストール不要
  • Ubuntu 24.04 では openjdk-21-jdk(バージョン 21.0.11+10)が apt から入る
  • GraalVM 本体は GitHub Releases からダウンロードして /opt に展開する手順が最も素直
  • ネイティブバイナリの起動時間は JVM の 195 倍高速(実測値)。CLI ツールやマイクロサービスに効果大
  • ビルドには build-essential(gcc)と最低 2GB の空きメモリが必要

動作確認済み環境

検証環境

本記事のコマンド・数値はすべて ghcr.io/graalvm/graalvm-community:21 Docker コンテナ(Ubuntu ベース)および ubuntu:24.04 Docker 公式イメージで実行した結果です。計測日: 2026-06-20。

項目 バージョン・詳細
Ubuntu 24.04.4 LTS (Noble Numbat)
GraalVM CE 21.0.2+13.1 (OpenJDK 21.0.2 ベース)
native-image 21.0.2(GraalVM CE 21 に同梱)
C コンパイラ gcc(native-image のビルドに必要)
実行環境 Docker コンテナ(ホストは Linux/amd64)

GraalVM と native-image が必要になる場面

通常の Java アプリは JVM(Java Virtual Machine)が起動してからコードを実行します。JVM 自体の初期化に 100〜300ms かかるため、コマンドラインツールや短命なマイクロサービスでは「起動が遅い」という問題が出ます。

GraalVM の native-image は、Java バイトコードをビルド時に AOT(Ahead-of-Time)コンパイルして、JVM を内包しないネイティブバイナリを生成します。結果として:

  • 起動時間が ミリ秒以下 になる(今回の実測では 1ms)
  • メモリ使用量が大幅に減る
  • コンテナの軽量化が実現できる(JRE 不要)

CLI ツール、Lambda/Cloud Functions、高頻度に起動・終了するマイクロサービスなどで特に効果があります。逆に長時間動かし続けるアプリサーバーは、JVM の JIT 最適化が効くため native-image のメリットが薄れます。

GraalVM CE 21 と native-image のバージョン確認(実測)
GraalVM CE 21 と native-image のバージョン確認(実測)

前提パッケージのインストール

GraalVM の native-image は C コンパイラ(gcc)を使ってネイティブコードを生成します。Ubuntu 24.04 では build-essential をインストールしておく必要があります。




ubuntu@linuxlab: ~
$ sudo apt update && sudo apt install -y build-essential curl
Reading package lists… Done
Building dependency tree… Done
The following NEW packages will be installed:
binutils cpp gcc make …
0 upgraded, 18 newly installed, 0 to remove and 0 not upgraded.

念のため、Ubuntu 24.04 の apt リポジトリで提供されている Java 関連パッケージを確認しておきましょう。

Ubuntu 24.04 の apt で利用できる Java パッケージ一覧(実測)
Ubuntu 24.04 の apt で利用できる Java パッケージ一覧(実測)

Ubuntu 24.04 では openjdk-21-jdk(Candidate: 21.0.11+10-1~24.04.2)が標準リポジトリから入ります。GraalVM CE はこれとは別物です。apt では提供されていないため、GitHub Releases から手動でダウンロードします。

GraalVM CE 21 のインストール

手順1:GraalVM CE をダウンロードする

GraalVM Community Edition は GitHub の oracle/graal リポジトリの Releases ページで配布されています(2026-06-20 時点の最新安定版は 21.0.2)。




ubuntu@linuxlab: ~
$ cd /tmp
$ curl -fLO https://github.com/oracle/graal/releases/download/vm-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 287M 100 287M 0 0 12.3M 0 0:00:23 0:00:23 –:–:– 13.1M

最新版を使う場合

GraalVM CE の最新 Releases は github.com/oracle/graal/releases で確認できます。ファイル名の 21.0.2 部分を実際のバージョンに置き換えてください。アーキテクチャが ARM64(Raspberry Pi・Apple Silicon 等)の場合は linux-x64linux-aarch64 に変えます。

手順2:/opt に展開して JAVA_HOME を設定する




ubuntu@linuxlab: ~
$ sudo tar -xzf /tmp/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz -C /opt
$ sudo ln -sf /opt/graalvm-community-java21 /opt/graalvm
# シンボリックリンクで /opt/graalvm を固定パスにする

次に /etc/environment または ~/.bashrc に環境変数を追加します。




ubuntu@linuxlab: ~
$ echo ‘export JAVA_HOME=/opt/graalvm’ >> ~/.bashrc
$ echo ‘export PATH=$JAVA_HOME/bin:$PATH’ >> ~/.bashrc
$ source ~/.bashrc

システム全体(全ユーザー)に適用する場合は /etc/profile.d/graalvm.sh として置くのが整理しやすいです。




ubuntu@linuxlab: ~
$ sudo tee /etc/profile.d/graalvm.sh <<‘EOF’
export JAVA_HOME=/opt/graalvm
export PATH=$JAVA_HOME/bin:$PATH
EOF
$ source /etc/profile.d/graalvm.sh

インストール確認

バージョンを確認します。




ubuntu@linuxlab: ~
$ java -version
openjdk version “21.0.2” 2024-01-16
OpenJDK Runtime Environment GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30)
OpenJDK 64-Bit Server VM GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30, mixed mode, sharing)
$ native-image –version
native-image 21.0.2 2024-01-16
GraalVM Runtime Environment GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30)
Substrate VM GraalVM CE 21.0.2+13.1 (build 21.0.2+13, serial gc)
$ echo $JAVA_HOME
/opt/graalvm-community-java21

GraalVM CE 21 系では native-image が最初から /usr/bin/native-image にリンクされており、昔のように gu install native-image は不要です。

GraalVM CE 21 バージョン確認サマリ(実測)
GraalVM CE 21 バージョン確認サマリ(実測)

HelloWorld をネイティブバイナリにコンパイル

実際に Java ソースを書いてネイティブバイナリを生成してみます。

手順1:Java ソースを用意する




ubuntu@linuxlab: ~/myapp
$ mkdir ~/myapp && cd ~/myapp
$ cat > HelloWorld.java <<‘EOF’
public class HelloWorld {
public static void main(String[] args) {
System.out.println(“Hello from GraalVM Native Image!”);
System.out.println(“No JVM required at runtime.”);
}
}
EOF

手順2:javac でコンパイルする




ubuntu@linuxlab: ~/myapp
$ javac HelloWorld.java
# HelloWorld.class が生成される(エラーなければ出力なし)
$ jar –create –file HelloWorld.jar –main-class HelloWorld HelloWorld.class
$ ls -lh HelloWorld.*
-rw-r–r– 1 user user 430 Jun 20 00:00 HelloWorld.class
-rw-r–r– 1 user user 1.5K Jun 20 00:00 HelloWorld.jar

手順3:native-image でネイティブバイナリを生成する




ubuntu@linuxlab: ~/myapp
$ native-image -cp . HelloWorld -o hello-native
======================================================================
GraalVM Native Image: Generating ‘hello-native’ (executable)…
======================================================================
[1/8] Initializing… (1.4s @ 0.18GB)
Java version: 21.0.2+13, vendor version: GraalVM CE 21.0.2+13.1
Graal compiler: optimization level: 2, target machine: x86-64-v3
Garbage collector: Serial GC (max heap size: 80% of RAM)
[2/8] Performing analysis… [*****] (2.5s @ 0.36GB)
3,240 reachable types / 15,717 reachable methods
[3/8] Building universe… (0.6s @ 0.39GB)
[4/8] Parsing methods… [*] (0.6s @ 0.27GB)
[5/8] Inlining methods… [***] (0.2s @ 0.30GB)
[6/8] Compiling methods… [**] (3.0s @ 0.33GB)
[7/8] Layouting methods… [*] (0.6s @ 0.39GB)
[8/8] Creating image… [*] (0.8s @ 0.44GB)
———————————————————————-
5.43MB (41.17%) for code area: 8,929 compilation units
7.50MB (56.86%) for image heap: 97,386 objects and 48 resources
265.72kB ( 1.97%) for other data
13.18MB in total
———————————————————————-
$ ls -lh hello-native
-rwxr-xr-x 1 user user 13M Jun 20 00:00 hello-native
$ ./hello-native
Hello from GraalVM Native Image!
No JVM required at runtime.

実際に動かしてみると、JVM の起動ウェイトが一切ないことがわかります。JAR ファイルが 1.5KB なのに対してネイティブバイナリは 13.18MB ですが、これは JVM のランタイムライブラリが全部バイナリに埋め込まれているためです。

JVM クラスファイル vs ネイティブバイナリの比較(実測)
JVM クラスファイル vs ネイティブバイナリの比較(実測)

ビルドプロセスの読み方

native-image のビルドは 8 ステージあります。慣れないうちはエラーが出てもどこで止まったかわかりにくいので、各ステージを頭に入れておくと助かります。

native-image ビルドプロセス(8ステージ)の実測ログ
native-image ビルドプロセス(8ステージ)の実測ログ
native-image ビルドログ詳細(実測)
native-image ビルドログ詳細(実測)
ステージ 処理内容 実測時間(HelloWorld)
[1/8] Initializing GraalVM・GC・コンパイラの初期化 1.4 秒
[2/8] Performing analysis 静的解析(到達可能なコード・型を特定) 2.5 秒
[3/8] Building universe 型システムの構築 0.6 秒
[4/8] Parsing methods メソッドの解析 0.6 秒
[5/8] Inlining methods インライン展開の決定 0.2 秒
[6/8] Compiling methods ネイティブコード生成(最も時間がかかる) 3.0 秒
[7/8] Layouting methods コードレイアウトの最適化 0.6 秒
[8/8] Creating image バイナリファイルの書き出し 0.8 秒

HelloWorld のような最小サンプルでも合計で約 10 秒かかります。Spring Boot など本格的なフレームワークを使うと 数分〜10 分以上 になります。CI/CD でビルドする場合はタイムアウト設定に注意が必要です。

native-image ビルド結果サマリ(実測)
native-image ビルド結果サマリ(実測)

起動時間ベンチマーク(実測)

「本当に速いのか」を確かめるために、JVM 実行とネイティブバイナリで 10 回ずつ起動時間を計測しました。

JVM vs ネイティブバイナリ 起動時間ベンチマーク(実測)
JVM vs ネイティブバイナリ 起動時間ベンチマーク(実測)
実行方法 平均起動時間 最小 最大 計測回数
java -cp . TimeApp 195.4 ms 111 ms 265 ms 10 回
./time-app(ネイティブ) 1 ms 1 ms 1 ms 10 回

195 倍の差。正直、ここまで数字がはっきり出るとは思っていませんでした。JVM は起動時に参照型のロード・JIT コンパイルの準備が入るため、どうしてもオーバーヘッドが避けられません。ネイティブバイナリはその初期化を丸ごとビルド時に済ませているので、即座にメイン処理に入れます。

ただし、JVM の JIT がウォームアップした後の スループット性能は JVM の方が高くなる場合があります。長時間動かすアプリサーバーでは、ネイティブバイナリが必ずしも有利とは限りません。

著者アイコン
著者アイコン

実測前は「20〜30 倍くらいかな」と思っていたのですが、1ms という数字を見てさすがに驚きました。コンテナ環境での CLI ツールに使うと体感差がかなり出そうです。

よくあるエラーと解決策

① Error: No C compiler found




ubuntu@linuxlab: ~
Error: No C compiler found for compilation of the bootstrap code.

build-essential が入っていない場合に出ます。sudo apt install build-essential で解決します。

② java.lang.OutOfMemoryError during image build

メモリ不足時の対処

native-image のビルドは最低 2GB、Spring Boot などの場合は 4〜8GB の RAM を要求します。VPS やコンテナでメモリが足りない場合は -J-Xmx4g オプションでヒープを指定するか、スワップを追加してください。

③ JAVA_HOME が旧 OpenJDK を指している




ubuntu@linuxlab: ~
$ java -version
openjdk version “21.0.11” 2024-10-15 ← apt の OpenJDK が優先されている
$ echo $JAVA_HOME
/usr/lib/jvm/java-21-openjdk-amd64 ← GraalVM ではない

~/.bashrcJAVA_HOME 設定が反映されていない場合、source ~/.bashrc するか、ターミナルを開き直してください。update-alternatives で apt の java が優先されている場合は sudo update-alternatives --config java で GraalVM を選択することもできます。

④ Reflection 関連の実行時エラー

Spring Boot・Hibernate など、実行時リフレクションを多用するフレームワークでは native-image のビルドが通っても実行時に ClassNotFoundException が出ることがあります。これはネイティブビルドでリフレクション対象を事前登録する必要があるためで、reflect-config.json の設定や、Spring Boot の場合は spring-boot-starter-aot プラグインが必要です。フレームワーク固有の AOT 対応手順をフレームワークのドキュメントで確認してください。

まとめ

Ubuntu 24.04 に GraalVM CE 21 をインストールして native-image を試しました。実測で確認できたことをまとめます:

  • GraalVM CE 21 系は native-image が同梱。gu install は不要
  • Ubuntu 24.04 では apt から直接入らないため、GitHub Releases からダウンロードして /opt に展開するのが最も確実
  • ネイティブバイナリの起動時間は JVM の 195 倍高速(195ms → 1ms、10 回計測の平均)
  • バイナリサイズは HelloWorld でも 13.18MB(JVM ランタイムが丸ごと入るため)
  • ビルド時に build-essential と最低 2GB の RAM が必要
  • 本格的なフレームワークを使う場合は AOT 対応の設定が別途必要

CLI ツールの起動待ちが気になっている場合、GraalVM native-image は有力な選択肢です。逆に長時間動かすサービスは JVM の JIT 最適化が効くので、無理にネイティブ化する必要はありません。

本格的に VPS 上で Java アプリを動かすなら、東京リージョンのある Vultr が遅延・コスト面でバランスがよいです。

コメント

タイトルとURLをコピーしました