Spring Bootアプリの起動が遅いと、ローカル開発でリビルドのたびに集中力が切れますし、Kubernetes上ではPodがreadiness probeに間に合わずに落とされる、なんてこともありますよね。本記事では GraalVM Native Image までは踏み込まずに、通常のJVM環境のままで起動を短くする手順をまとめました。

この記事のTL;DR

  • まず --debug と Actuator の /actuator/startup でボトルネックを可視化する
  • 効果が大きい順は「依存整理 → AutoConfig除外 → CDS+AOT → 遅延初期化(主にローカル)」
  • 本番では CDS と AOT の併用が副作用が少なく定常的に効く
  • それでも遅い場合は Kubernetes の startupProbe で猶予を確保する

細かい手順や落とし穴は各セクションで詳しく見ていきます。

起動が遅くなる主な原因を押さえる

まずは何に時間がかかっているかの当たりを付けましょう。Spring Bootの起動で重くなりがちなのは、だいたい次のあたりです。

  • ClasspathスキャンとAutoConfigurationの条件評価
  • 多すぎる spring-boot-starter-* 依存
  • DataSourceやHibernateの初期化、外部接続待ち
  • 広すぎる @ComponentScan 範囲

推測で手を入れる前に、計測から始めるのが鉄則です。

—debug と ApplicationStartup で計測する

手っ取り早く全体像を見るには --debug オプションが便利です。

java -jar app.jar --debug

これで CONDITIONS EVALUATION REPORT が出力され、どのAutoConfigurationが有効で、なぜ無効になったかが分かります。

さらにステップ単位で時間を取りたいときは BufferingApplicationStartup を仕込みます。

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(MyApplication.class);
    app.setApplicationStartup(new BufferingApplicationStartup(2048));
    app.run(args);
}

バッファに各ステップの所要時間が記録されるので、後述の Actuator から取り出せます。Actuatorを入れずに自前で読みたい場合は FlightRecorderApplicationStartup をJFRに流す手もあります。

Actuator startup エンドポイントで可視化

startupエンドポイントのJSONを読み解く

/actuator/startup のレスポンスは概ね次の形です(一部抜粋)。

{
  "springBootVersion": "3.x",
  "timeline": {
    "startTime": "2026-05-13T08:00:00.000Z",
    "events": [
      {
        "startupStep": { "name": "spring.beans.instantiate", "tags": [{"key":"beanName","value":"dataSource"}] },
        "startTime": "...",
        "endTime": "...",
        "duration": "PT0.420S"
      }
    ]
  }
}

実用上は events[]duration の降順でソートし、spring.beans.instantiate / spring.context.refresh / spring.boot.application.starting といったステップ名で grep するのが手早いです。jq で取り出すなら次のようなワンライナーが便利です。

curl -s localhost:8080/actuator/startup \
  | jq '.timeline.events | sort_by(.duration) | reverse | .[0:10]'

BufferingApplicationStartup と FlightRecorderApplicationStartup の使い分け

ApplicationStartup実装は主に2種類あります。

  • BufferingApplicationStartup(capacity): メモリにステップを溜め、Actuatorで取り出す。開発・調査用capacity を超えると古いステップが捨てられる点に注意(Spring Boot標準アプリなら2048で大抵足りますが、Bean数の多いアプリでは4096〜8192推奨)。
  • FlightRecorderApplicationStartup: JFRイベントとして記録する。本番でJFRを常時有効化している環境なら、Actuatorを露出せずに済むのが利点。JDK Mission Control などで他の起動メトリクスと突き合わせられます。

どちらも SpringApplication#setApplicationStartup で差し替えるだけで切り替えられるため、ローカルでは Buffering、本番では FlightRecorder、という運用がやりやすいです。

spring-boot-starter-actuator を入れて、startup エンドポイントを公開します。

management.endpoints.web.exposure.include=startup

起動後に GET /actuator/startup を叩くとJSONでステップ一覧が返ってきます。timeline.events[].duration を降順で並べれば、ボトルネックの本命がすぐ見えます。本番で公開する場合は、必ず認可で保護するか、内部ネットワーク限定にしてください。

spring.main.lazy-initialization の効果と落とし穴

もっとも手軽な施策が遅延初期化です。

spring.main.lazy-initialization=true

全Beanの生成が初回利用時まで遅延されるので、起動だけ見れば確実に速くなります。ただし副作用もあって、

  • 初回リクエストのレイテンシが悪化する
  • 設定ミスによる起動時エラーが、本番の最初のリクエストまで顕在化しない

といった落とし穴があります。ローカル開発やCIでは積極的に有効化、本番では @Lazy で個別のBeanに絞る、という使い分けが現実的です。

不要な AutoConfiguration を除外する

条件評価レポートの Positive matches を眺めて、使っていない機能を見つけたら除外します。

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
import org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration;

@SpringBootApplication(exclude = {
    JmxAutoConfiguration.class,
    WebSocketServletAutoConfiguration.class
})
public class MyApplication { }

プロパティでまとめて指定する書き方もあります。

spring.autoconfigure.exclude=\
  org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration

JMX、WebSocket、Mail、Redis あたりは「依存に入っているが使っていない」典型例です。除外したら必ず回帰テストを通して、想定外のBeanが消えていないか確認しましょう。

コードで一元管理したいなら @SpringBootApplication(exclude=...)、プロファイルや環境変数で切り替えたいなら spring.autoconfigure.exclude、と用途で使い分けるのがおすすめです。AutoConfigurationの仕組み自体は Spring BootのAutoConfigurationの仕組み もどうぞ。

Component スキャンと依存関係を整理する

@SpringBootApplication をルートパッケージに置きっぱなしにしていると、配下の全パッケージをスキャンします。basePackages を明示するだけでも、それなりに効きます。

@SpringBootApplication(scanBasePackages = "com.example.app.api")
public class MyApplication { }

あわせて pom.xmlbuild.gradle の Starter依存を棚卸しして、未使用のものを外しておくと土台から軽くなります。

Spring Boot 3.3+ の CDS を有効化する

JVM標準の Class Data Sharing は、クラスメタデータを事前に共有アーカイブ化することで読み込みを高速化します。Spring Boot 3.3 から正式にサポートされました。

手順は2段階です。まず1回トレーニングランを実施してアーカイブを作ります。

java -XX:ArchiveClassesAtExit=app.jsa \
     -Dspring.context.exit=onRefresh \
     -jar app.jar

java -XX:SharedArchiveFile=app.jsa -jar app.jar

-Dspring.context.exit=onRefresh は、Applicationコンテキストの refresh が終わった直後にプロセスを終了させるためのオプションです。実サービスを起動せずアーカイブだけ生成したいので、トレーニングランではこれを指定します。

短縮率はクラス数やJDKバージョンに左右されるので断言は難しいですが、Spring公式ブログなどでも二桁%程度の改善報告があり、副作用がほぼないため優先度の高い施策です。Dockerで使う場合は、生成した app.jsa をイメージに含めておくのを忘れずに。

GraalVMを使わない AOT モード

AOTで何が事前生成されるのか

processAot を実行すると、target/spring-aot/main/ (Gradleなら build/generated/aotSources) 配下に次のような成果物が出力されます。

  • *__BeanDefinitions.java: 各Beanの定義をJavaソースとして事前生成したもの。実行時の @Configuration クラス解析や条件評価をスキップできます。
  • reflect-config.json / resource-config.json / proxy-config.json: 反射・リソース・プロキシのヒント情報。GraalVMだけでなく通常JVMでも、AOT実行時に参照されます。
  • META-INF/native-image/.../native-image.properties: ビルド時に検出した起動オプション。

ビルド時にApplicationContextを一度refreshして、その時点で確定できるBean構成・条件分岐の結果をコード化しているのが本質です。

ビルド時に確定する設定と、実行時に切り替えられない設定

spring.aot.enabled=true で実行すると、以下はビルド時の値で固定されます(Spring Boot 3.x 前提)。

  • @ConditionalOnProperty / @ConditionalOnClass などの条件評価結果
  • @Profile によるBean有効化判定(spring.profiles.active をビルド時に指定する必要あり)
  • @Value で参照されるプロパティのうち、ビルド時に解決可能なもの

つまり「dev/prodで構成Beanが入れ替わる」「環境変数で機能フラグを切る」運用は、AOTビルドをプロファイル別に分けるか、AOTを諦めるかの選択になります。Maven なら次のようにプロファイル付きで AOT を実行します。

./mvnw -Pprod spring-boot:process-aot \
  -Dspring.profiles.active=prod

Native Image との関係

AOTモードはNative Imageの前段としても使われますが、本記事のように 通常JVMで実行する AOT はNative Imageと別物です。比較すると次のようになります。

項目AOTモード(JVM)GraalVM Native Image
起動時間短縮10〜30%程度90%以上
ピーク性能JVMと同等JITが効かないため劣る場合あり
メモリ使用量JVMと同等大幅減
ビルド時間数秒〜十数秒数分
動的機能リフレクション制限なしリフレクション/プロキシは事前登録必須

「Native Image までは踏み切れないが起動短縮はしたい」というケースで、JVM上のAOT+CDS併用は現実的な落とし所です。

Spring Boot は GraalVM Native Image を使わなくても、AOT処理だけを通常JVMで活用できます。Beanのメタデータを事前生成し、起動時の反射処理を減らす仕組みです。

実行時は次のプロパティを有効化します。

spring.aot.enabled=true

ただしこれ単体では効きません。ビルド時に Maven なら spring-boot-maven-pluginprocess-aot ゴール、Gradle なら processAot タスクを実行して、AOT成果物を生成しておく必要があります。

./mvnw spring-boot:process-aot
# Gradleの場合
./gradlew processAot

AOTを有効化すると、プロファイルなど一部の設定が起動時ではなくビルド時に確定されるため、実行時にプロファイルを切り替える運用とは相性が悪い点に注意してください。CDS と組み合わせれば効果が積み上がるので、本番向けには両方有効化するのが王道です。Native Image との違いについては Spring BootでGraalVM Native Imageを使う方法 も参考にしてください。

Kubernetes では startup probe を活用する

それでも起動に数十秒かかる場合は、Kubernetes側で猶予を確保しましょう。startupProbe を設定すると、起動完了までは livenessProbe の判定が抑制されます。

startupProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  failureThreshold: 30
  periodSeconds: 5
readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  periodSeconds: 10
livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  periodSeconds: 10

Actuator の /health/liveness/health/readiness を活用すれば、Spring Boot の起動完了状態を素直にprobeに反映できます。Actuator自体の使い方は Spring Boot Actuator入門 を参照してください。

計測 → 施策 → 再計測のサイクルを回す

最後に進め方の話です。施策を一気に入れると、何がどれくらい効いたのか分からなくなるので、必ず1つずつ適用します。経験的に効果の大きい順は次のあたりです(カッコ内は本文中の対応見出し)。

  1. 依存整理と Component スキャン範囲の絞り込み(「Component スキャンと依存関係を整理する」)
  2. 不要な AutoConfiguration の除外(「不要な AutoConfiguration を除外する」)
  3. CDS と AOT の併用(「Spring Boot 3.3+ の CDS」「GraalVMを使わない AOT モード」)
  4. 遅延初期化(「spring.main.lazy-initialization」、主にローカル・CI向け)

CDS/AOT を遅延初期化より上位に置いているのは、本番でも副作用が少なく定常的に効くためです。ただしアプリ構成によって順位は前後するので、ベースラインは複数回計測して平均を取り、1施策ずつ効果を確かめるのが結局いちばん近道です。

まとめ

起動高速化は派手な単一施策よりも、計測ベースで小さな改善を積み上げるのが結局いちばん効きます。まずは Actuator の startup エンドポイントでボトルネックを見える化し、依存整理 → AutoConfig除外 → CDS/AOT の順で試してみてください。GraalVM に踏み込まなくても、十分実用的な短縮が狙えます。

関連記事

起動高速化と合わせて、運用フェーズで効くトピックも整理しておくと役立ちます。