Project Valhalla と JDK 28: Java へバリュークラスを導入する

Java の基本設計—ほぼすべてが参照型であること—は「フラッフィー」なメモリレイアウトを生み出します。プログラマがオブジェクトの配列を作成すると、JVM はオブジェクトを連続して格納せず、ヒープ上に散在するオブジェクトへのポインタ(参照)の配列を格納します。

このアーキテクチャは主に 2 つのパフォーマンスボトルネックを引き起こします:

  1. ポインタ間接参照: オブジェクトフィールドへのすべてのアクセスはポインタを介した「ホップ」を必要とし、キャッシュミスのリスクが高まります。
  2. オブジェクトオーバーヘッド: ヒープ上のすべてのオブジェクトはヘッダー(型情報や同期用メタデータ)を持ち、かなりのメモリを消費します。

最新の CPU は主記憶装置より何桁も高速であり、参照の局所性が重要になります。データが密に格納されている場合、1 つの CPU キャッシュライン(通常 64 バイト)で複数の値を同時にロードできます。現在の参照中心のモデルでは、CPU がメモリ取得を待たされることが多く、データ集約型アプリケーションのパフォーマンスが大幅に制限されます。

JEP 401: バリュークラスとバリューオブジェクト

JDK 28 は value 修飾子を導入し、バリュークラスの作成を可能にします。これらのクラスのインスタンスは バリューオブジェクト であり、固有の同一性ではなく状態によって定義されます。

バリュークラスの主な特徴

  • 同一性なし: 同じフィールド値を持つ 2 つのバリューオブジェクトは置換可能とみなされます。固有のメモリアドレスは持ちません。
  • 暗黙的に final: バリュークラスのすべてのインスタンスフィールドは暗黙的に final です。
  • 同期不可: 同一性がないため、バリューオブジェクトに対して同期を取ることはできず、試みると IdentityException がスローされます。
  • 参照型ステータス: JDK 28 ではバリュークラスは参照型のままであり、null になることが可能です。null 不許可のバリュー型は将来の JEP で計画されています。

等価性の変更

value オブジェクトに対する == 演算子の意味が変わります。メモリアドレス(同一性)を比較する代わりに、==置換可能性—両方の値が同じクラスに属し、同じフィールド値を持つかどうか—をチェックします。ビジネスロジック上の等価性には、引き続き equals() が推奨されます。

パフォーマンスメカニズム: スカラー化とフラッテン化

オブジェクトの同一性要件を排除することで、JVM はバリューオブジェクトを 2 つの主要な手法で最適化できます:

1. スカラー化

この JIT コンパイラ最適化はバリューオブジェクトを構成フィールドに「分解」します。オブジェクトへのポインタを渡す代わりに、JVM は生のフィールド(例: Color オブジェクトの 3 バイト)を CPU レジスタに直接渡します。これによりオブジェクト割り当てが実質的に「無料」になり、ガベージコレクタ(GC)の負荷が減ります。

2. ヒープフラッテン化

JVM はバリューオブジェクトのデータをポインタなしでフィールドや配列要素に直接エンコードできます。これにより、バリューオブジェクトの配列がプリミティブ配列(例: int[])と同様に連続したデータブロックとして格納され、密なメモリレイアウトが実現します。

アトミック性制約: フラッテン化が行われるには、データが同時アクセス時の「分割」防止のために通常は原子的に読み書き可能である必要があります。現在のプラットフォームでは、これによりフラッテン化は 64 ビット以内(null フラグを含む)に収まるオブジェクトに限定されます。将来的な更新(例: 128 ビットエンコーディングや null 制限型)が実装されない限り、より大きなオブジェクトは通常のオブジェクトとしてヒープに残ります。

専門化ジェネリクスへの道

JDK 28 がバリュークラスを導入したものの、ジェネリクスにおける 型消去 の問題はまだ解決されていません。現在、List<Point>Point オブジェクトへの参照を格納したままで、型パラメータ T が実行時に Object に消去されるため、バリューオブジェクトはヒープ上に実体化されます。

Project Valhalla の長期的な解決策は 2 つのフェーズに分かれています:

  • ユニバーサルジェネリクス: 型変数がバリュー型をカバーできるようにする言語レベルの変更(API の専門化に備える新しいコンパイラ警告を導入)。
  • 専門化ジェネリクス: 将来の JVM 拡張で、具体的な型引数に対して専門化されたクラスレイアウトを生成し、最終的に ArrayList<Point>Point[] と同等の効率になるようにします。

JDK 28 の提供概要

機能 JDK 28 におけるステータス 備考
value modifier Preview value クラス/レコードの宣言を可能にする
Scalarization Preview 割り当てを排除する JIT 最適化
Heap Flattening Preview 小さなバリューオブジェクトの密な格納
Boxed Primitives Preview IntegerLong などがバリュークラスになる
Non-nullable Types Not Included 将来の JEP で計画中
Specialized Generics Not Included 将来のリリースで計画中

コミュニティの視点と技術的トレードオフ

リリースに関する技術的議論は、いくつかの重要な点を強調しています:

== for value classes は基本的に memcmp() のようになります。これはカプセル化を破壊し、実装の詳細を露呈させるため、やや残念です。 — @layer8

関数が java.lang.Object に消去される値 x を持つ場合... 以前は null かどうかをチェックし、オブジェクトに対して同期を取るのが安全でした。これはもう安全ではありません: これにより IdentityException が直ちにスローされる可能性があります。 — @leiroigh

批評家は、アトミックフラッテン化の 64 ビット制限が非常に小さなデータ型にしか恩恵をもたらさない可能性があると指摘しています。他方、最初のフェーズでバリュークラスを nullable な参照型として保持する決定は、ユーザーモデルを簡素化するものの、厳格な「プリミティブクラス」モデルと比べてパフォーマンス上限を下げると主張しています。しかし、賛同者はこの段階的アプローチが、Java の巨大な既存エコシステムと下位互換性を維持するために必要であると論じています。

Sources