アルファテックブログ

JDK 25を活用したJavaアプリケーションのパフォーマンス最適化

カバー

はじめに

アプリケーションのパフォーマンスは、高品質なソフトウェアを考えるうえで非常に重要な要素です。
メモリ使用量やCPU使用率を削減することで、必要なハードウェアスペックを低く抑えられ、導入コストを軽減できます。アプリケーションが高速で起動し、応答速度が速ければ、一度により多くのリクエストに対応できます。また開発の観点からも、起動が速ければ動作確認の時間を短縮でき、メモリ使用量が少なければ開発環境でのリソース消費も最小限に抑えられます。

Javaのパフォーマンス改善手法にはさまざまなものがありますが、今回はJDK 21以降で強化されたCDS(Class Data Sharing) や2025年9月にLTSとしてリリースされるJDK 25で追加されるCompact Object Headers に焦点をあてて調査し、検証ではJavaアプリケーションの起動速度とメモリ使用量について確認を行いました。

この記事では、実施した調査と検証の結果を紹介し、パフォーマンス改善について考察します。

起動高速化のアプローチ

Javaアプリケーションでは、コンパイル時にソースコードを直接ネイティブコードにするのではなく、バイトコードと呼ばれる中間コードに変換します。アプリケーションの実行時に、JVMが起動されバイトコードを解釈します。そしてパフォーマンスが重要な部分(よく参照される部分)は実行中に必要に応じてネイティブコードへの変換処理を行っています。

バイトコードを使ったこれらの仕組みは、異なるOS上でアプリケーションを動作させるために必要であり、Javaを用いたアプリケーション開発のメリットです。しかし、起動時にバイトコードを解析したり、必要に応じて変換したりする分、起動時処理が増えて起動速度が遅くなってしまいます。

では、どのような場合に問題となるでしょうか。例えば、起動時間に応じて料金がかかるクラウドインフラで動作するアプリケーションでは、必要のないタイミングでは停止してコストを削減したいです。そういった場合には頻繁にアプリケーションの再起動を行う可能性があります。このような場合、アプリケーションを頻繁に再起動することが想定されるため、毎回発生する起動時処理によって起動速度が遅いと、全体のパフォーマンスに大きな影響を与えます。
そこで本記事では、起動時間の短縮が期待できる以下の技術を取り上げ、実際に検証を行います。

  • クラス・データ共有(CDS)
  • Ahead-of-Time Class Loading & Linking(AOTキャッシュ利用)
  • Oracle GraalVM

メモリ使用量削減のアプローチ

Javaのアプリケーションでは、ヒープ領域と呼ばれるメモリ空間について不要になったメモリを自動で開放するガベージコレクション という機能があります。
ガベージコレクションはヒープ領域が一定のサイズを超えたときに自動で実行されますが、実行時に処理が停止する関係上、ガベージコレクションの回数はなるべく減らす必要があります。ガベージコレクションの処理の効率化という手もありますが、今回はメモリ使用量を削減するアプローチとして、JDK 25で追加されたCompact Object Headersについて紹介し、実際に検証を行います。

パフォーマンス改善機能の紹介

起動高速化

Ahead-of-Time Class Loading & Linking

Ahead-of-Time Class Loading & Linking (JEP 483) は、JDK 24で実験的に追加された、Javaの起動速度を向上させるための新しい機能です。
アプリケーション起動時のクラスファイル解析のコストを削減することで、起動速度を向上させる ものです。以前から存在するクラス・データ共有(CDS) を拡張する仕組みであり、まずはCDSについて簡単に紹介します。

【CDS】
CDSは、Javaアプリケーションの起動時間の削減複数JVM間のメモリフットプリントの削減 に役立つ機能です。
通常、JVMではアプリケーションの起動時にクラスファイル(バイトコード)がロードされます。CDSはあらかじめメタデータ情報をCDSアーカイブファイルに保存し、JVMの起動時にこれをメモリへマッピングすることで、クラスロードおよび初期化の処理を高速化します。

JDK 12以降では、Javaのコアライブラリのみを含むデフォルトのアーカイブがJDKに事前にパッケージされており、アプリケーション実行時に自動的に利用されます。このアーカイブは複数のJVMで読み取り専用として共有でき、同一ホスト上で動作する複数のJavaプロセスで共有することで、メモリ消費を削減できます。

【AppCDS(Application CDS)】
AppCDSはCDSの拡張で、デフォルトアーカイブに含まれないアプリケーション固有クラスをCDSアーカイブに含めて共有可能にします。AppCDSではアーカイブファイルの作成をアプリケーションの起動前に専用のコマンドで実施し、起動時に生成したアーカイブファイルを指定して利用します。

さらに、アプリケーション実行時にCDSアーカイブを動的に作成できるDynamic CDSも導入されており、より簡易な運用が可能になっています。 この記事の検証では、Dynamic CDSアーカイブファイルを使用する方法でCDSの効果を確認しています。

【Ahead-of-Time Class Loading & Linking】
アプローチはCDSと同様で、クラスロードの処理を短縮することで起動高速化を図ります。
CDSではクラスファイルの読み込みと解析を行いますが、Ahead-of-Time Class Loading & Linkingでは読み込み、解析、さらにロードとリンクまでを実施し、キャッシュファイル(AOTキャッシュ)を生成します。アプリケーション起動時に生成したAOTキャッシュを利用することで、リンクまでの処理をスキップして実行できるため、CDSに比べさらなる起動の高速化が実現できます。

CDS用のアーカイブファイルと同様、AOTキャッシュの作成には事前にコマンドの実行が必要です。JDK 24では、この作成コマンドがAOT構成の記録のために1回、記録した構成をもとにキャッシュを作成するために1回と、計2回コマンドの実行が必要でした。CDSでは1つのコマンドでアーカイブ作成が実行できていたため、ユーザの操作が1ステップ増えています。また、初めのAOT構成記録コマンドで出力されるAOT構成用の一時ファイルが残ってしまい、不要であれば別途削除する必要がある、という問題もありました。
この問題はJDK 25で改善され、AOT構成の作成からキャッシュの生成までを1つのコマンドで実行できるようになりました。また、一時ファイルも自動的に削除されるようになっているため、不要なファイルを別途削除する手間もなくなりました。

CDSとAhead-of-Time Class Loading & Linkingの違い

CDSとAhead-of-Time Class Loading & Linkingの高速化範囲

【利用時の注意点】
AppCDSやAhead-of-Time Class Loading & Linkingを利用する際は、実際にアプリケーションを動作させる環境に近い環境でAOTキャッシュおよびCDSアーカイブを作成する必要があります。 特に、圧縮OOP(Compressed Ordinary Object Pointer)や後述するCompact Object Headersを利用する場合は、注意が必要です。

圧縮OOPとは、オブジェクトに対する管理ポインタであるOOPのサイズを圧縮する機能です。64ビット環境ではデフォルトで有効になっていますが、アプリケーションのヒープサイズが32GBを超えると無効になります。圧縮OOPが有効な状態で生成したAOTキャッシュやCDSアーカイブは、無効になった場合には再作成が必要です。

Compact Object Headersを利用する際も、有効・無効の状態を統一したうえでAOTキャッシュやCDSアーカイブを作成してください。

また、JDK 25ではCompact Object Headers利用時のデフォルトCDSアーカイブファイル(classes_coh.jsa)が事前にパッケージされる予定です。これにより、Compact Object Headersを指定した実行時でも、デフォルトのCDSが有効になると考えられます。今回検証に使った、プレビュー版のAmazon Corretto 25 (25.0.0.34.1)には含まれていましたが、プレビュー版のGraalVM 25(25-dev+35.1)にはclasses_coh.jsaは含まれていませんでした。プレビュー版であるため、正式リリース版やアップデートで追加される可能性があります。

Oracle GraalVM

Oracle GraalVM(以下、GraalVM) とは、Ahead-of-Time(AOT)ネイティブコンパイル機能を持ったJDKです。JITコンパイラとしてGraalコンパイラをデフォルトで使用しており、アプリケーションのパフォーマンスの最適化を行います。

【Graalコンパイラ】
GraalVM上で動作するJIT(Just-In-Time)コンパイラです。Graal JITコンパイラを従来のJava VM C2コンパイラの代わりに利用することができます。
Graalコンパイラでは従来のC2コンパイラとは異なるコードの分析と最適化を行います。公式ドキュメントでは、部分エスケープ解析最適化が紹介されており、オブジェクトが外部からアクセス可能かどうかを判断し、外部からアクセスされない場合はヒープへの割り当てを行わないといったメモリの効率化が図られています。

今回は、パフォーマンス最適化が起動高速化にどのように影響するかを検証しました。

【ネイティブイメージ】
GraalVMはネイティブイメージ生成機能も備えています。ソースコードを事前にネイティブ実行ファイルにコンパイル(AOTコンパイル)することで、JVMを介さずにアプリケーションを起動できます。
ネイティブイメージの実行ファイルは通常の実行ファイル(JARファイル)に比べて、JVMを利用せず、必要最低限のコードのみが含まれるため軽量です。また、コンパイル時にアプリケーションの初期化処理の一部を実行するため、起動を高速化できます。
事前にコンパイルする都合上、リフレクションなどの動的にアクセスされる要素があれば事前に準備が必要であり、ビルドも通常のビルドよりも時間がかかります。今回は、検証環境で利用しているライブラリが一部対応しておらず、計測は実施していません。

メモリ使用量の削減

Compact Object Headers

JDK 25から、Compact Object Headers (JEP 450, JEP 519) が正式に追加されました。
Compact Object Headersを有効化すると64ビットアーキテクチャ上で12~16バイト(=96~128ビット)だったオブジェクトのヘッダーサイズを、8バイト(=64ビット)まで削減することができます。
オブジェクトあたりのヘッダーサイズが減少することで、オブジェクトサイズが減少し、結果としてヒープサイズが削減されることとなります。また、データの局所性が向上することで、CPUのキャッシュアクセス時のヒット率が向上し、アプリケーションの高速化も期待できます。JDK 24で実験的に追加された機能でしたが、JDK 25で正式なオプションとしてリリースされます。

【オブジェクトヘッダーとは】
ヒープに格納されるオブジェクトにはメタデータが存在し、JVMはこれをオブジェクトのヘッダーに格納しています。
ヘッダーのサイズは一定で、64ビット版のJVMでは設定に応じて12~16バイトのサイズになっています。小さなオブジェクトを数多く利用する場合は、1オブジェクトにつきサイズ固定のヘッダーが付与されることになり、このサイズをわずかにでも縮小することがアプリケーション全体のメモリ消費量削減につながります。

通常、ヘッダーはマークワードとクラスワードという領域に分かれていますが、コンパクトオブジェクトヘッダーを有効化すると、クラスワード領域に格納されていたクラスポインターという情報を圧縮し、マークワード領域に組み込みます。

検証では、実際にオプションを有効化し、メモリ使用量にどのような変化が起こるのかを確認しました。

検証

前章で紹介した機能を使用し、パフォーマンスが改善するかを検証しました。

検証環境について

本検証では、Spring Boot 3.5.4を用いたアプリケーションを対象にしました。 各検証ではJDKバージョンを変更し、それぞれ計測しています。 今回使用したJVMのバージョンは以下になります。

利用したJDK

JDK名バージョン
Amazon Corretto 21(LTS)21.0.8.9.1
Amazon Corretto 25(プレビュー版)(LTS)25.0.0.34.1
GraalVM 21(LTS)21.0.8+12.1
GraalVM 25(プレビュー版)(LTS)25-dev+35.1

アプリケーション実行環境であるOSの基本的な情報は以下となります。

項目内容
CPUコア数12
メモリ12GB
OSWindows 11 Pro 24H2

起動高速化の検証

CDSおよびAhead-of-Time Class Loading & Linkingの検証

起動高速化が期待されるAhead-of-Time Class Loading & Linkingの検証を実施します。
Ahead-of-Time Class Loading & Linkingは、CDS機能の進化であるため、CDS機能との速度比較も行いました。また、Amazon Corretto 21におけるCDSの速度も一緒に計測をしています。

【検証パターン】
以下に検証パターンを列挙しました。

  • 検証パターン
    • Amazon Corretto 21
      • デフォルト起動
      • JARの事前解凍
      • CDS
    • Amazon Corretto 25
      • デフォルト起動
      • JARの事前解凍
      • CDS
      • Ahead-of-Time Class Loading & Linking(ロード・リンク無効)
      • Ahead-of-Time Class Loading & Linking

【手順】
各検証パターンでの手順について紹介します。

〇デフォルト起動

  1. 起動時のオプションを指定せずにJARを実行します。
    Terminal window
    $ java -jar app.jar

〇JARの事前解凍

  1. 作成したJARを以下のコマンドで展開しておきます。

    Terminal window
    $ java -Djarmode=tools -jar app.jar extract --destination destinationPath

    ※--destinationオプションで展開先ディレクトリを指定できます。

  2. 実行時に展開したJARを指定し、実行します。

    Terminal window
    $ java -jar ".\destinationPath\app.jar"

〇CDS

  1. 作成したJARの事前解凍を実施します。 ※事前解凍をすることで、CDSを効率的に行うことができます。
  2. Dynamic CDSアーカイブファイルを作成します。
    Terminal window
    $ java -XX:ArchiveClassesAtExit=application.jsa "-Dspring.context.exit=onRefresh" -jar .\destinationPath\app.jar
    ※-XX:ArchiveClassesAtExitで出力するアーカイブファイルパスを指定します。
    ※-Dspring.context.exit=onRefresh を指定することで、アプリケーションが起動後に自動で停止します。
  3. アーカイブファイルを指定して、アプリケーションを起動します。
    Terminal window
    $ java -XX:SharedArchiveFile=application.jsa -jar .\destinationPath\app.jar

〇Ahead-of-Time Class Loading & Linking

  1. 作成したJARの事前解凍を実施します。
    ※事前解凍することで、Ahead-of-Time Class Loading & Linkingを効率的に行うことができます。
  2. AOTキャッシュを作成します。
    Terminal window
    $ java -XX:AOTCacheOutput=app.aot "-Dspring.context.exit=onRefresh" -jar .\destinationPath\app.jar
  3. AOTキャッシュを指定して、アプリケーションを起動します。
    Terminal window
    $ java -XX:AOTCache=app.aot -jar ".\destinationPath\app.jar"

〇Ahead-of-Time Class Loading & Linking(ロード・リンク無効)

  1. 前述のAhead-of-Time Class Loading & Linkingの手順のAOTキャッシュの作成まで実施します。
  2. AOTキャッシュを指定し、ロードとリンクを無効化するオプションをつけてアプリケーションを起動します。
    Terminal window
    $ java -XX:AOTCache=app.aot -XX:-AOTClassLinking -jar ".\destinationPath\app.jar"

【結果】
上記の手順で起動時間を各パターン10回ずつ計測し、平均起動時間を算出しました。 結果は以下のようになりました。

アプリケーション起動にかかった平均時間(秒)

起動方法Amazon Corretto 21Amazon Corretto 25
デフォルト24.4317.95
事前解凍23.3516.41
CDS17.4914.47
Ahead-of-Time Class Loading & Linking(ロード・リンク無効)-13.63
Ahead-of-Time Class Loading & Linking-12.90

デフォルトよりCDS、Ahead-of-Time Class Loading & Linkingを有効化した方がアプリケーション起動速度は向上しています。CDSを有効化した場合ではAmazon Corretto 21において約28.5%の削減、Amazon Corretto 25において約19.4%の削減が行われており、Ahead-of-Time Class Loading & Linkingを有効化した場合ではAmazon Corretto 25において約28.2%の削減がみられます。
また、Amazon Corretto 25においてはデフォルトの動作でもAmazon Corretto 21に比べて約26.6%の削減されています。

【考察】
上記の結果から想定通り、CDSのアーカイブやAOTキャッシュを利用することで、起動時処理の短縮分だけアプリケーションの起動が高速化されることがわかりました。
一方で、AOTキャッシュを利用する場合は、事前にキャッシュを作成する必要がある点にも注意が必要です。今回の検証ではAOTキャッシュの作成にAmazon Corretto 25で平均80.5秒かかりました。これは必要なクラス数によって変動しますが、今回の起動時間と比較すると約6.3倍かかっており、コストがかかる作業となります。

それでは、AOTキャッシュの再作成が必要なタイミングはいつになるのでしょうか。JARの変更時には必ず再作成が必要なのか、それともクラスロードに影響しないメソッド内部の変更のみの場合は再作成は不要なのか、検証しました。AOTキャッシュの作成について動作をみるために、以下のような手順でアプリケーションを起動してみました。

  1. AOTキャッシュ作成する。
  2. コードの変更(あるクラスのローカル変数の初期化値の変更)をし、コンパイルを実施する。
  3. JARの事前解凍を行ったあと、AOTキャッシュの作成は行わず、コード変更前に作成したAOTキャッシュを利用して起動する。

起動すると、キャッシュ処理中にエラーが発生した旨のログが出力されました。

[warning][aot] This file is not the one used while building the shared archive file: 'app.jar', timestamp has changed, size has changed
[error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
[error ][aot] shared class paths mismatch (hint: enable -Xlog:class+path=info to diagnose the failure)
[error ][aot] Unable to map shared spaces

AOTキャッシュ作成時と起動しているJARが異なる旨のエラーであり、メソッド内にとどまるコード変更でも、再度キャッシュ作成を行わない限りエラーが発生するという結果となりました。ただ、エラーは発生したもののアプリケーションの起動はできており、変更内容が正しく反映されていることを確認しました。この時の起動時間を10回計測したところ、平均16.93秒でありJARの事前展開を実施したときの速度に近いため、AOTキャッシュを利用しない通常起動になっていたと考えられます。
同様にCDSアーカイブについてもエラーが発生しましたが、アプリケーションの起動は可能で、起動時間は平均16.94秒でした。CDSについても、CDSアーカイブを利用しない通常起動になっていたと推測されます。

以上より、AOTキャッシュやCDSアーカイブはJARの変更のたびに再作成が必要となります。開発時のように頻繁にビルド・実行を繰り返す状況では、コード変更から起動までの時間が長くなるためメリットは限定的です。
一方で、本番環境など同じJARを繰り返し起動する場合は、起動時間を大幅に短縮できるため、特に従量課金制のクラウドプラットフォームなどでの運用に適していると考えられます。

なお、Ahead-of-Time Class Loading & Linkingなどを利用しないデフォルト状態においても、Amazon Corretto 25がデフォルトの起動でAmazon Corretto 21に比べて約26.6%も起動時間が削減されています。このことから、Amazon Corretto 25へのアップデートを行うだけでも起動速度が顕著に改善されていることがわかります。
こちらの理由については調査しましたが、明確に断定できる情報は得られませんでした。
調査の中でAmazon Corretto 24(24.0.2.12.1)での速度を計測したところ、10回平均27.12秒でした。これにより、Amazon Corretto 24では速度改善が確認できなかったことから、速度の顕著な改善はAmazon Corretto 25から始まっていると考えられます。

GraalVMの検証

次にGraalVMを用いてGraalコンパイラを利用した場合の起動速度を計測しました。
※今回、GraalVMのネイティブイメージ機能は、検証環境で利用しているライブラリが一部対応しておらず、検証しておりません。

こちらはプレビュー版であるGraalVM 25とGraalVM 21で検証しました。 また、CDSおよびAhead-of-Time Class Loading & Linkingと併用した場合、どの程度起動時間が向上するのかについても計測しました。

【検証パターン】
以下に検証パターンを列挙しました。

  • 検証パターン
    • GraalVM 21
      • Graalコンパイラ未使用
      • Graalコンパイラ使用(デフォルト)
      • Graalコンパイラ使用+CDS
    • GraalVM 25
      • Graalコンパイラ未使用
      • Graalコンパイラ使用(デフォルト)
      • Graalコンパイラ使用+CDS
      • Graalコンパイラ使用+Ahead-of-Time Class Loading & Linking

【手順】
GraalVMのJITコンパイラのデフォルトはGraalコンパイラなので、オプション指定なしで実行します。
Graalコンパイラを未使用にする場合は-XX:-UseJVMCICompilerオプションをつけて実行します。
この場合、HotSpot C2コンパイラが使用されます。

Terminal window
$ java -XX:-UseJVMCICompiler -jar ".\target\app.jar"

GraalVM 25でCDSおよびAhead-of-Time Class Loading & Linkingを有効化した際、起動時に以下のエラーが発生しました。

CDSを有効化して起動したとき:

[0.039s][error][cds] An error has occurred while processing the shared archive file. Run with -Xlog:aot,cds for details.
[0.040s][error][cds] Mismatched values for property jdk.module.addmods: jdk.internal.vm.ci specified during runtime but not during dump time
[0.040s][error][cds] Disabling optimized module handling

Ahead-of-Time Class Loading & Linkingを有効化して起動したとき:

[0.069s][error][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
[0.070s][error][aot] Mismatched values for property jdk.module.addmods: jdk.internal.vm.ci specified during runtime but not during dump time
[0.070s][error][aot] Disabling optimized module handling

Ahead-of-Time Class Loading & Linkingに関しては、--add-modules=jdk.internal.vm.ciのオプションを付けることでAOTキャッシュの作成およびアプリケーション実行が成功しましたが、CDSに関しては同様のオプションを設定してもエラーは解消されませんでした。
今回はAhead-of-Time Class Loading & Linking有効化時との併用の効果が確認できれば十分なため、GraalVM 25についてはAhead-of-Time Class Loading & Linking有効の場合のみ計測しています。

Ahead-of-Time Class Loading & Linking有効化での起動は以下で実施しています。

Terminal window
$ java --add-modules=jdk.internal.vm.ci -XX:AOTCacheOutput=app.aot "-Dspring.context.exit=onRefresh" -jar .\destinationPath\app.jar
$ java --add-modules=jdk.internal.vm.ci -XX:AOTCache=app.aot -jar ".\destinationPath\app.jar"

【結果】
各パターン10回ずつ計測し、平均起動時間を算出しました。 結果は以下のようになりました。

アプリケーション起動にかかった平均時間(秒)

起動方法GraalVM 21GraalVM 25
Graalコンパイラ未使用30.1819.09
Graalコンパイラ使用18.3118.66
Graalコンパイラ+CDS15.79
Graalコンパイラ+Ahead-of-Time Class Loading & Linking-12.66

※ 起動時にエラーが発生したのでスキップ

Amazon Corretto使用時の起動時間は以下の通りです。(再掲)

起動方法Amazon Corretto 21Amazon Corretto 25
デフォルト24.4317.95
CDS17.4914.47
Ahead-of-Time Class Loading & Linking-12.90

GraalVM 21を用いた際には、Graalコンパイラ未使用時と比べて起動時間を約39.3%短縮できています。しかし、GraalVM 25ではGraalコンパイラ未使用時の速度が速く、ほぼ起動速度は同じという結果になりました。
CDSおよびAhead-of-Time Class Loading & Linkingを有効化した場合、GraalVM 21ではCDSアーカイブを使用したとき、GraalVM 25ではAOTキャッシュファイルを使用したときに、それぞれ起動速度が向上する結果となりました。

【考察】
GraalVM 21の結果をみると、Graalコンパイラを使用した場合にAmazon Correttoの同一バージョンより起動速度が若干速くなっているようですが、一方で、GraalVM 25の結果からはそのような傾向はみられずほぼ同程度の速度でした。
今回はプレビュー版での検証であり、正式版や今後のアップデートで各種設定の調整や不具合修正等により内容が変更される可能性があります。

また、CDSやAhead-of-Time Class Loading & Linkingも同時に有効化することで起動時間短縮につながることがわかります。しかし、GraalVM 25では同時使用する際にエラーが発生し、jdk.internal.vm.ciをモジュールとして認識させるオプションを付ける必要がありました。こちらも今後修正される可能性はありますが、機能を組み合わせて利用する際には別途作業が必要となる場合があるため、適切な設定を行ったうえで併用する必要があります。

今回の検証では、GraalVM 21において起動時間の改善がみられました。また、GraalVM 25についても、今後のアップデート次第では同様に改善がみられる可能性があります。GraalVMのJITコンパイラ導入は、利用するJDKを変更するだけで可能であり、簡単に導入できます。

メモリ使用量削減の検証

Compact Object Headers

Compact Object Headersを有効化した場合のメモリ使用量の変化を確認します。
今回はヒープ領域の使用量が減ったことを確認するための指標として、一定時間でのガベージコレクションの頻度を確認しました。

【検証方法】
まず、Amazon Corretto 25でコンパクトオブジェクトヘッダーを有効化して10分放置し、ガベージコレクション(以下、GC)の頻度を計測しました。
しかし、通常利用の状態で計測してもGCの頻度に明確な変化は確認できませんでした。
Compact Object Headersは小さいオブジェクトが多数存在する状況で特に効果を発揮します。したがって、条件をそろえなければ効果を観測できない可能性があります。そのため、多数のオブジェクトが生成される状態を再現し、計測を行いました。具体的には、Stringのみをフィールドに持つクラスを作成し、そのインスタンスを500万生成するエンドポイントを追加しました。そのエンドポイントへ1秒ごとにリクエストを3分間し続け、3分間の間でGC回数を計測する形で実施しました。

【検証パターン】
以下に検証パターンを列挙しました。

  • 検証パターン
    • Amazon Corretto 21
      • デフォルト起動
    • Amazon Corretto 25
      • デフォルト起動
      • Compact Object Headers有効

【手順】
Compact Object Headersの有効化は簡単で、起動時にオプションを指定することで可能です。

Terminal window
$ java -XX:+UseCompactObjectHeaders -jar .\target\app.jar

【結果】
各パターンで5回計測し、平均GC回数を測定した結果、以下のようになりました。

3分間のGC発生回数(回)

起動方法Amazon Corretto 21Amazon Corretto 25
デフォルト1797.21934.6
Compact Object Headers有効-1874.2

3分間のGC合計停止時間と3分間のGC最大停止時間も測定をしましたが、JDKによる変化はありませんでした。

【考察】
Amazon Corretto 25においては、実際にGC頻度が減ることを確認でき、Compact Object Headersの効果が出ていることがわかりました。

また、今回気になる結果として、Amazon Corretto 21がAmazon Corretto 25環境よりもGC頻度が少なくなるという結果になりました。こちらについては調査をしましたが、原因の特定に至りませんでした。まだプレビュー版であるため、正式リリース版での改善や今後のアップデートでの改善がみられる可能性があります。

また、Compact Object Headersはメモリ効率の面で多くの利点があるので、特に小さなオブジェクトが大量に生成されるアプリケーションでは、動作を十分に確認したうえで有効化を検討してもよいのではないかと考えられます。

終わりに

今回は、Javaアプリケーションのパフォーマンス改善についてJDK 25から正式に追加される機能も交えて検証を行いました。検証の結果としては、Ahead-of-Time Class Loading & LinkingやCompact Object Headersが起動速度改善やメモリ使用量削減において実際に効果を発揮する機能であることがわかりました。一方で、Ahead-of-Time Class Loading & Linkingは開発時の運用ではかえってコード修正から起動までの時間を増加させてしまう側面があったり、Compact Object Headersも通常の使用状態では効果が表れにくいものだったりと必ずしも必須のオプションではないと考えられます。

JDK 25については、バージョンアップだけで起動速度が向上する結果となりました。パフォーマンス改善によるソフトウェア品質向上、開発効率化の観点からも、JDK 25へのアップデートは早期に実施を検討してもよいものと考えられます。

また、GraalVMについては今回は計測を実施してはいませんが、ネイティブイメージがより大きな効果を発揮するものであると考えています。しかし、その性質上適用のハードルが高くなっています。Graalコンパイラについても、起動速度の改善を確認しましたが他のパフォーマンスについても最適化されて改善されている可能性があります。こちらも今後の検証で計測し、総合的にJDK変更を検討することが望ましいです。

今回の検証環境では、JDK 25への更新とCompact Object Headersは計測の結果、効果がみられましたので、実際に利用を検討できるものと思います。Ahead-of-Time Class Loading & Linkingは起動速度の改善は確認できましたがAppCDSアーカイブやAOTキャッシュの用意に時間がかかることから開発運用方法については慎重に検討する必要があり、GraalVMについてはGraalVM 25における他のパフォーマンス項目の検証やネイティブイメージについてのさらなる調査、検証が必要だと感じました。

今回の記事では、特定の検証環境下での計測となっているため、読者の皆様が想定しているアプリケーションではまた違った効果が表れる可能性があります。少しでも、この記事が今後のパフォーマンス改善を検討するうえでの手助けとなれば幸いです。

参考


TOP
アルファロゴ 株式会社アルファシステムズは、ITサービス事業を展開しています。このブログでは、技術的な取り組みを紹介しています。X(旧Twitter)で更新通知をしています。