LambdaやKubernetes環境でSpring Bootアプリのコールドスタートが遅くて困っていませんか?JVMの起動に数秒かかる問題は、GraalVM Native Imageを使うと劇的に改善できます。Spring Boot 3.0から正式サポートされたので、今が導入のチャンスです。
GraalVM Native Imageとは
GraalVM Native ImageはJavaコードをAOT(Ahead-of-Time)コンパイルしてネイティブバイナリを生成する技術です。JVM起動が不要になるため、FaaSや短命コンテナとの相性が抜群です。
Spring Boot 3.0以前は「Spring Native」という実験的プロジェクトが存在しましたが、設定が複雑でした。3.0以降は標準機能として取り込まれ、かなり使いやすくなっています。Spring Boot 2.xからの移行についてはSpring Boot 2から3への移行ガイドも参考にしてください。
JVMモードとネイティブモードの性能比較
シンプルなSpring Bootアプリ(Hello World相当)での計測例です。依存が増えると数値は変わりますが、傾向は同じです。
| 項目 | JVMモード | ネイティブモード |
|---|---|---|
| 起動時間 | 3〜5秒 | 0.1〜0.3秒 |
| メモリ使用量 | 200〜400MB | 50〜100MB |
ただし スループットやレイテンシはJVMが有利 なケースも多いです。JITコンパイルで最適化が積み重なるJVMは、長時間稼働する高トラフィックAPIサーバーでは依然として強力です。ネイティブビルドは「起動が速くてメモリを使わない」ことが求められる場面に向いています。
環境セットアップ
GraalVM JDK 21が必要です。SDKMANを使うと簡単にインストールできます。
# 利用可能なバージョンを確認(バージョン番号は執筆時点の例)
sdk list java | grep graal
# Oracle GraalVM(JDK 17以降、GFTCライセンスで無償)
sdk install java 21.0.2-graal
sdk use java 21.0.2-graal
# GraalVM CE(コミュニティ版)を使う場合は graalce サフィックスを使う
# sdk install java 21.0.2-graalce
native-image --version
21.0.2-graal はOracle GraalVM、21.0.2-graalce はコミュニティ版です。バージョン番号は sdk list java | grep graal で最新版を確認してから選択してください。Spring Bootは 3.1以上 を推奨します。
ビルド設定の追加
Mavenの場合
spring-boot-starter-parent を使っていれば、native-maven-plugin を追加するだけです。バージョンはBOMが管理するため指定不要です。
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Gradleの場合
plugins {
id 'org.springframework.boot' version '3.2.0'
// io.spring.dependency-management は Spring Boot Gradle プラグイン使用時は任意
id 'org.graalvm.buildtools.native'
}
Spring Boot BOMがプラグインバージョン(0.10.x系)を管理するため、バージョン指定は不要です。
ネイティブイメージのビルド
# Maven
./mvnw -Pnative native:compile
# Gradle
./gradlew nativeCompile
ビルドには 10〜20分 かかることがあります。成功すると target/<artifactId>(Maven)または build/native/nativeCompile/(Gradle)にバイナリが生成されます。CI環境では専用のビルドステップを用意しましょう。
AOT処理とReflection問題
ネイティブビルドでは、ビルド時にBeanグラフ全体を静的解析します。問題になるのが 動的Reflection です。JacksonやJPAなどはリフレクションで動いているため、設定なしではビルドが通っても実行時にエラーが出ることがあります。Spring Boot自体はほとんどのケースでAOTヒントを自動生成してくれますが、問題が起きるのは主にサードパーティライブラリやカスタムコードです。
Reflectionヒントの追加
アノテーションで指定する(一番シンプル)
@Configuration
@RegisterReflectionForBinding(MyDto.class)
public class AppConfig {
}
reflect-config.jsonを手書きする
[
{
"name": "com.example.MyDto",
"allDeclaredFields": true,
"allDeclaredMethods": true,
"allDeclaredConstructors": true
}
]
src/main/resources/META-INF/native-image/ に配置します。RuntimeHintsRegistrar インターフェースを使ったプログラム的な登録も可能で、条件付きでヒントを追加したいときに便利です。
native-image-agentで設定を自動生成する
手動でヒントを書くのは大変ですよね。native-image-agent を使えば、JVMモードで実行しながらReflection使用状況を記録できます。
java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image \
-jar target/myapp.jar
アプリを起動して代表的なエンドポイントを一通り叩いてから、Ctrl+C(SIGINT)で正常終了 させてください。強制終了(SIGKILL)では設定ファイルが書き出されません。終了後に reflect-config.json などが生成されます。実際に実行されたコードパスしか記録されない 点に注意してください。複数回実行して設定をマージしたい場合は config-merge-dir オプションが便利です。
Testcontainersでの動作確認
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
Gradleの場合は testImplementation 'org.springframework.boot:spring-boot-testcontainers' を追加します。@ServiceConnection アノテーションを使うと、データベース等のコンテナがテスト実行時に自動起動します。ネイティブモードでのテストは次のコマンドで実行します。
./mvnw -PnativeTest test
ネイティブテストはJVMテストより数倍時間がかかるため、CI/CDでは専用ステージに分けるのが現実的です。
詰まりやすいビルドエラーと解決策
ClassNotFoundException / Missing reflection configuration
実行時にクラスが見つからないエラーはReflectionヒントの設定漏れです。native-image-agent で再生成するか、@RegisterReflectionForBinding で対象クラスを追加してください。
ビルドメモリ不足
JAVA_TOOL_OPTIONS=-Xmx8g ./mvnw -Pnative native:compile
-J-Xmx8g をmvnwコマンドに直接渡す方法はMaven自身のJVMヒープを制御するだけで、native-imageコンパイラが起動するサブプロセスのヒープには影響しません。環境変数 JAVA_TOOL_OPTIONS を使うか、native-maven-plugin の <buildArgs> に <buildArg>-J-Xmx8g</buildArg> を追加する方法が正しいです。大規模プロジェクトでは8GB程度必要になることもあります。
ライブラリがネイティブ非対応
一部のライブラリはネイティブビルドに対応していません。GraalVM Reachability Metadata Repository で対応状況を確認し、代替ライブラリへの置き換えか除外を検討しましょう。
Lambda・Kubernetesへのデプロイ
マルチステージDockerfileでビルドと実行を分離します。
FROM ghcr.io/graalvm/native-image-community:21 AS builder
WORKDIR /app
COPY . .
RUN ./mvnw -Pnative native:compile -DskipTests
FROM ubuntu:22.04
COPY --from=builder /app/target/myapp /myapp
ENTRYPOINT ["/myapp"]
ランタイムに ubuntu:22.04 を使うのは、デフォルトのネイティブバイナリがglibc動的リンクになるためです。--static オプションで静的リンクビルドにすれば gcr.io/distroless/static も利用できますが、追加設定が必要です。詳細はSpring BootのDockerコンテナ化ガイドやKubernetesデプロイガイドを参考にしてください。
まとめ
ネイティブビルドが向いているのは、Lambda・短命コンテナ・CLIツールなど 起動速度とメモリ効率が重要 な場面です。一方で、長期稼働の高スループットAPIサーバーはJITコンパイルが効くJVMモードの方が有利なことが多いです。ビルド時間が長くなりデバッグもやや難しくなるため、用途を見極めて導入しましょう。ライブラリの対応状況も事前に確認しておくと安心です。
- Reflectionエラーには
@RegisterReflectionForBindingかnative-image-agentで対処 - ビルドメモリ不足は
JAVA_TOOL_OPTIONS=-Xmx8gで解決(-J-XmxはMaven自身のヒープ制御なので注意) - スループット重視の場面ではVirtual Threads(Java 21)との組み合わせも検討してみてください
まずは新規プロジェクトや小さなサービスから試してみましょう。