From JIT to Native: Path to Efficient Java Containers

原文はこちら。
The original article was written by Olga Gupalo (Member of Tech Staff for GraalVM at Oracle).
https://medium.com/graalvm/from-jit-to-native-path-to-efficient-java-containers-d81221418c39

Micronautアプリケーションがミリ秒以内で起動し、ほとんどのGoアプリケーションよりも小さなコンテナで実行できるのでしょうか?はい、可能です。GraalVM Native Imageを使えばね。ここでは、伝統的なJavaアプリケーションを、高速起動、軽量かつ配布可能な、クラウドネイティブコンテナ対応のアプリケーションに変換した方法を紹介します。

Home – Micronaut Framework
https://micronaut.io/
Native Image
https://www.graalvm.org/latest/reference-manual/native-image/

まずMicronaut webサーバーから実験を開始し、22MBの完全に静的なデプロイ可能イメージで終了しました。どのように実現したのでしょうか?GraalVM Native Image、スマートなリンク戦略、そして最後の魔法の一手を使っています。

その過程で、さまざまなビルド戦略をテストし、トレードオフを検証し、実際の影響を測定しました。

しかし、JARにコンパイルして完全なJDKを含むDockerコンテナに配置すると、このwebサーバーは最大470MBにまで膨れ上がりました。この結果は、複数のOpenJDKディストリビューションで確認されました。マイクロサービスやKnative、AWS Lambdaのようなスケールゼロプラットフォームで軽量化を追求する場合、これは現実的な選択肢ではありません。

どれくらい小さくできるのでしょうか?その答えは、実は非常に小さくできます。ただし、「ネイティブ化」し、さらに「静的ネイティブ化」する覚悟があれば、です。

Step by Step: Slimming Down the Container

まず、アプリケーションの様々なパッケージ化と実行方法のベンチマークから始めました。徐々にJVMをカスタムランタイム、ネイティブ実行可能ファイル、そして最終的に完全に静的バイナリに置き換えていきました。この記事で示した結果は、一貫性を確保するため、48GBのメモリと4つのCPUを搭載したOracle Linux 8マシン上で収集したものです。

このデモはオープンソースであり、再現可能です。

From JIT to Native: Efficient Java Containers with GraalVM and Micronaut
https://github.com/graalvm/workshops/tree/main/native-image/micronaut-webserver

各ステップはスクリプトとDockerfileで自動化されており、正確なベンチマークを可能にするため、アプリケーションは実際のGraalVMドキュメントページを配信するようにしています。

GraalVM documentation pages served with Micronaut

最も注目すべき段階の簡単な概要をまとめました。

Step 1: The Starting Point — Running a JAR in a Container

JAR ファイルとしてコンパイルし、java -jarで実行したこのwebサーバーは問題なく動作しましたが、コンテナのサイズが依然として大きいままでした。OpenJDKディストリビューションをスリムなOSコンテナにインストールして作成したコンテナの代わりに、distrolessのjava21-debian12イメージを使用することで、コンテナのサイズを216MBまで削減できました。ただし、静的webページのファイルが全体のイメージサイズに寄与している点に注意してください。

🧱 Image: gcr.io/distroless/java21-debian12
🚀 Startup: ~378ms
📦 Container size: 216MB

Javaベースのフル機能のWebサーバーとしては悪くないのですが、コールドスタートとメモリ使用量(フットプリント)の最適化がさらに必要です。

Step 2: Creating a Custom JDK with jlink

次に、jlinkをテストしました。このツールは、アプリケーションが必要とするモジュールのみを含む軽量化されたJavaランタイムを作成します。jdepsを使用して依存関係を特定し、jlinkでランタイムを構築することで、かなりのスペースを節約できました。

🧱 Image: gcr.io/distroless/java-base-debian12
🚀 Startup: ~340ms
📦 Container size: 167MB

以下はコマンドのフローです。

RUN ./mvnw clean package
RUN ./mvnw dependency:build-classpath -Dmdep.outputFile=cp.txt
RUN CP=$(cat cp.txt) && \
    MODULES=$(jdeps --ignore-missing-deps -q --recursive --multi-release 24 --print-module-deps --class-path "$CP" target/webserver-0.1.jar) && \
    echo "Modules: $MODULES" && \
    jlink \
      --module-path "${JAVA_HOME}/jmods" \
      --add-modules "$MODULES",jdk.zipfs \
      --verbose \
      --strip-debug \
      --compress zip-9 \
      --no-header-files \
      --no-man-pages \
      --strip-java-debug-attributes \
      --output jlink-jre

jlinkにより、不要なモジュールを削除するだけで、即座に49MBの容量削減を実現しました。パフォーマンスの劇的な変化はありませんが、効率化に向けた着実な一歩です。

Step 3: Compiling Ahead-of-Time — Going Native

次にGraalVM Native Image、ゲームチェンジャーの登場です。Native Image Mavenプラグインを使用して、マルチステージDockerビルド内でアプリケーションをAOTビルドを実施して、その後実行用のdistrolessベースイメージにパッケージ化しました。

Maven plugin for GraalVM Native Image
https://graalvm.github.io/native-build-tools/latest/maven-plugin.html

このケースではJVMは不要です。生成されたイメージは動的リンクされていました。

🧱 Image: gcr.io/distroless/java-base-debian12
🚀 Startup: ~20ms
📦 Container size: 132MB

この方法では、起動時間がほぼ1/17に短縮され、コンテナサイズは35MBにまで削減できました!これがネイティブコンパイルの真の力です。それでも、さらにスペースの節約を追求しました。

Step 4: Optimizing a Native Image for File Size

ネイティブ実行ファイル自体を小さくできたらどうでしょうか?GraalVMでは、パフォーマンスにコストがかかる最適化を省略することでファイルサイズを最適化する-Osフラグを提供しています。これをビルドに追加しました。

🧱 Image: gcr.io/distroless/java-base-debian12
🚀 Startup: ~20ms
📦 Container size: 102MB
📦 Binary size: 62MB (down from default 86MB!)

バイナリサイズが24MBにまで減少しました。動作や起動時間に変化はありません。最適化成功ですね。

さらに、GraalVM for JDK 24で導入された新しいSkipFlow機能をテストしました。

SkipFlow: Producing Smaller Executables with GraalVM
https://medium.com/graalvm/skipflow-producing-smaller-executables-with-graalvm-f18ca98279c2
https://logico-jp.dev/2025/06/21/skipflow-producing-smaller-executables-with-graalvm/

これは、実行されない可能性のあるコードパスを削減するために、到達不可能な分岐を追跡する静的解析最適化です。実験的な機能ですが、有効化は簡単でした。

-H:+TrackPrimitiveValues -H:+UsePredicates

今回は1MBの削減に留まりましたが、コードベースが大きくなるにつれ効果は拡大する可能性があります。

Step 5: Running a Mostly Static Application — Going Static

これまで、動的にリンクされたネイティブイメージを作成してきました。では、ほぼすべてのライブラリを静的にリンクしたらどうなるでしょうか?--static-nolibcフラグを使用すると、glibc以外のすべてのライブラリに対して静的にリンクされた実行可能ファイル(most static executable)を作成できます。これにより、より小さなコンテナイメージ(base-debian12)への移行が可能になりました。

🧱 Image: gcr.io/distroless/base-debian12
🚀 Startup: ~20ms
📦 Container size: 89.7MB

ベースコンテナを変更するだけで、さらに12MBのメモリが節約されました。このビルドにおけるnative-maven-pluginの設定の詳細は以下の通りです。

<plugin>
  <groupId>org.graalvm.buildtools</groupId>
  <artifactId>native-maven-plugin</artifactId>
  <version>${native.maven.plugin.version}</version>
  <configuration>
    <imageName>webserver.mostly-static</imageName>
    <buildArgs>
      <buildArg>--static-nolibc</buildArg>
      <buildArg>-Os</buildArg>
    </buildArgs>
  </configuration>
 </plugin>

Step 6: Running a Fully Static Application — In an Empty Container

ここからが本題です。--static --libc=muslフラグを使用すれば、OSレベルでの依存関係のない完全な静的ネイティブイメージを構築できます。つまり、1個のバイナリのみで動作します。これにより、scratchコンテナ(基本的には空のファイルシステム)を使用できるようになります(scratchは公式Dockerイメージ)。

🧱 Image: scratch
🚀 Startup: ~20ms
📦 Container size: 69.2MB

Micronaut webアプリが、69MB未満で本番環境向けにデプロイされ、ミリ秒単位で起動しました!これは多くのコンパイル済みC++アプリケーションよりも優れています。

Step 7. Going Extreme — UPX Compression

さらに小さくできるでしょうか?私たちは、完全に静的な実行可能ファイルにバイナリ圧縮ツールのUPXを適用し、同じscratchコンテナにパッケージ化しました。

UPX: the Ultimate Packer for eXecutables
https://upx.github.io/

UPXは最初にイメージを解凍するためCPU負荷が増加しますが、イメージサイズを大幅に削減します。

🧱 Image: scratch
🚀 Startup: ~20ms
📦 Container size: 22.3MB
📦 Binary size: 20MB (down from 62MB!)

元のコンテナサイズに比べてほぼ1/20に小さくなりました。アプリは依然として瞬時に起動し、リクエストを問題なく処理しました!ただし、その代償として可視性が失われるため、圧縮された内容を簡単に確認できません。

Before and After: The Numbers

では、開始時点からここまで状況を比較してみましょう。

20× reduction in size and 17× faster startup from our original setup — without sacrificing functionality

Engineering Lessons Learned

このプロセスから得た学びは以下の通りです。

すべての最適化で同じ効果をもたらすわけではないjlinkは有用ですが、Native Imageはサイズと起動時間の両面で桁違いの改善を実現します。
静的リンクにより、超小型コンテナを実現できるフルOSが不要な場合はscratchを使用しましょう。これにより、イメージサイズが小さくなるだけでなく、攻撃対象領域も縮小できます。scratchの代替として、gcr.io/distroless/staticalpine:3にいくつかのユーティリティとライブラリを追加した構成も検討できます。
ベースイメージのサイズが重要java21-debian12からbase-debian12またはscratchに変更することで、大幅な改善が得られました。この点を軽視しないように注意してください。
圧縮は最後の手段UPXは驚くほど効果的で、試す価値がありますが、パフォーマンスに敏感なアプリではCPUオーバーヘッドをテストする必要があります。

最後に、クラウド向けのJavaアプリケーションを開発していて、まだGraalVM Native Imageを試したことがない方は、今が絶好の機会です。エコシステムは整い、ツールも成熟しており、その成果は無視できないものとなっています。

Try It Yourself 💻

デモのソースコード、Dockerfile、および手順書はGitHubで公開されています。

From JIT to Native: Efficient Java Containers with GraalVM and Micronaut
https://github.com/graalvm/workshops/tree/main/native-image/micronaut-webserver

Micronaut ではなくSpringを使用したい場合は、このデモの Spring Boot 版が同じ内容で用意されています。

From JIT to Native: Efficient Java Containers with GraalVM and Spring Boot
https://github.com/graalvm/workshops/tree/main/native-image/spring-boot-webserver

より多くの実験や例については、Tiny Java Containersをご覧ください。

Tiny Java Containers
https://github.com/graalvm/graalvm-demos/tree/master/native-image/tiny-java-containers

ご意見やご感想は、Slack または GitHub までお寄せください。

Slack invitation
https://www.graalvm.org/slack-invitation
GitHub Issues
https://github.com/oracle/graal

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください