フューチャー技術ブログ

Java 24 & 25 連載: Java 24におけるパフォーマンス周りのアップデート

はじめに

Java 24 & 25のリリース連載第 2 弾の記事です。

第 1 弾の記事 に引き続き本記事では Java 24 のアップデートを取り上げます。

JEPs

Java 24 で対応された主要な 21 の JEP(Java Enhancement Proposals)は次の通りです。
Oracle の プレスリリース をベースにカテゴライズしています。

[object Object] undefined

第一弾の記事ではライブラリ系(JEP 484, JEP 485)およびセキュリティライブラリ系(JEP 478, JEP 496, JEP 497)を中心に取り上げましたが、本記事ではパフォーマンス系を中心に取り上げます。

JEP 483: Ahead-of-Time Class Loading & Linking

https://openjdk.org/jeps/483

クラスのローディングとリンクを起動前にキャッシュしておき、次回以降の起動を速くするしくみが追加されました。

どのようなしくみか

具体的には次のような流れでキャッシュの生成、再利用を実現します。

  1. トレーニング実行
    レコードモード(-XX:AOTMode=record)にてアプリケーションを一度起動し、どのクラスが読み込まれリンクされるかを記録します。
    次の例では app.aotconf に設定を記録しています。

    $ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -cp app.jar com.example.App ...
  2. キャッシュ生成
    クリエイトモード(-XX:AOTMode=create)にて、トレーニングモードで記録した設定ファイルからキャッシュを生成します。
    次の例では app.aot にキャッシュを記録しています。

    $ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot -cp app.jar
  3. キャッシュ利用
    アプリケーション起動時には、キャッシュを指定して起動することでクラス読み込み・解析・リンクにかかる時間を削減できます。

    $ java -XX:AOTCache=app.aot -cp app.jar com.example.App ...

なお、キャッシュが存在しない場合やトレーニング時と実行時でクラスパスやモジュール構成が変わっているなど問題がある場合は、従来どおり just-in-time(必要になった時点でクラスをロードする/リンクする)方式となります。

どれぐらい高速化が見込めるのか

JEP 483 の中では Stream API を利用するプログラムでの検証結果が紹介されていました。
非常に短いプログラムですが、Stream API を利用することで約 600 ものクラスが読み込まれています。

import java.util.*;
import java.util.stream.*;

public class HelloStream {

public static void main(String ... args) {
var words = List.of("hello", "fuzzy", "world");
var greeting = words.stream()
.filter(w -> !w.contains("z"))
.collect(Collectors.joining(", "));
System.out.println(greeting); // hello, world
}

}

このプログラムは Java 23 では 0.031 秒で実行されますが、Java 24 で AOT キャッシュを作成した結果 0.018 秒で実行され、約 42%程度の改善が見られました。
シンプルなプログラムでみるとそのインパクトはわずかに思えるかもしれませんが、Spring の PetClinic アプリケーションでは起動時間が 4.486 秒から 2.604 秒になったという結果も紹介されています。サーバレスなランタイムにおいては Java アプリケーションの起動速度が課題視されますが、この改善は大きな意味を持つのではないでしょうか。

JEP 490: ZGC: Remove the Non-Generational Mode

https://openjdk.org/jeps/474

ZGC において非世代別 GC(non-generational mode)が削除され、世代別 GC(generational mode)が唯一かつデフォルトのモードになりました。

ZGC は、Java 21(JEP479: Generational ZGC)にて世代別 GC がサポートされ、Java 23(JEP474: ZGC: Generational Mode by Default)にて世代別 GC がデフォルトになりました。

Java23 連携 でも紹介しましたが ZZGC や世代別 GC について、ここでも再掲して整理しておきます。

そもそも ZGC とは何か

ZGC は、Oracle が開発したガベージコレクタ(GC)で、スケーラブルで低レイテンシ(数テラバイト級の非常に大きいヒープでもアプリケーションの最大停止時間が 10ms 程度)というのが特徴です。

Java 9 以降、G1GC がデフォルトの GC として使われていますが、巨大なヒープを取り扱うアプリケーションが登場してきたことなどを背景に、従来の GC と比べてモダンな GC として登場しました。(cf. ざっくりわかった気になるモダン GC 入門

Java 11(JEP333)で試験的に導入され、Java 15(JEP377)にて正式リリースされ、今に至ります。

世代別 GC とは何か

世代別 GC 自体は特に新しい概念ではありません。
世代別 GC とは、ヒープ内のオブジェクトを寿命によって分類(Young 世代や Old 世代)し、ガベージコレクション(GC)の効率を向上させるしくみです。(cf. Java の GC の仕組みを整理する

ZGC は、低遅延であることを最優先に、ヒープ全体のコレクションを極力並行処理で行うことを目的として、当初は世代別 GC のアプローチを採用しませんでした。
そこから、より効率的なメモリ管理などさらなる最適化を目指して、世代別 GC の概念を取り入れていったという形になります。

JEP 491: Synchronize Virtual Threads without Pinning

Java 21 で導入された Virtual Thread(仮想スレッド) は OS のスレッド(Platform Thread)とは別の、JVM 内で管理されるより軽量なスレッド実装であり大量のスレッドを効率的に管理するために設計されたしくみです。
この Virtual Thread においては synchronized を含むプログラムを実行すると、処理が OS のスレッドに固定化されてしまう問題がありましたが、本アップデートで改善された形になります。

本アップデートについては次の記事が大いに参考になりましたので、ここでの説明は割愛させていただきます。

https://chiroito.hatenablog.jp/entry/2025/03/22/124631

おわりに

本記事では Java 24 のアップデートの中から、特にパフォーマンスに関わる JEP を中心に紹介しました。
次回は Java 25 の新機能を取り上げます。