2026/07/03 3:56
JEP 539:JVM の厳格なフィールド初期化がプレビューフェーズに移行
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
Java は、「strictly-initialized fields」と呼ばれるプレビュー用 VM の機能を導入しており、任意の final フィールドを読み取る前に明示的な初期化を行わなければならないことを強制します。これにより、以前にバグを隠蔽していた
null、0、または false などのデフォルト値への依存が排除されます。このメカニズムは、新しいバイトコードフラグ(ACC_STRICT_INIT)を使用して、静的なフィールドはクラスローディング前に、インスタンスフィールドは super() コンストラクタが完了した後には初期化されていることを確保し、一貫した読み取り動作を保証します。未初期化の静的フィールドへの読み取りや、深い反射による変異 (setAccessible) を通じて strictly-initialized な final フィールドを変更するなど、これらの不変条件に違反すると、ランタイム例外やバイトコード検証エラーが発生します。これらのフィールドを信頼可能なソースとして扱うことで、Java のコンパイラ(JIT)はキャッシュされた値を使用してパフォーマンス最適化を行うことができます。この機能は、初期化の安全性の責任を負うことを開発者に委ねる一方で、Value Classes および Null-restricted fields 用の最適化を可能にし、コンパイラおよび VM ツールチェーンの両方で特定のプレビュー有効化フラグが必要です。本文
JVM における厳密なフィールド初期化(Strict Initialization)
概要
Java 仮想マシン(JVM)において、厳密に初期化されたフィールドを導入する機能です。
- 参照される前に必ず初期化されるため、
や0
のようなデフォルト値は観測されません。null
な厳密な初期化フィールドについては、常に同一の値が観測されます。final- コンパイラからクラスファイルを出力させる際に利用可能な、プレビュー版 VM の機能です。
目標と非目標
目標
- 設計者に対し、既存モデルよりも強力な整合性保証を備えたフィールド初期化を提供する。
- 各クラスフィールドごとに、新しいモデルを採用するか、または既存のモデルを継続するかを選べる柔軟性を付与する。
非目標(目指さないこと)
- Java の言語機能自体(例:厳密な初期化用修飾子)を導入しない。
のコンパイル戦略を変更し、既存ソースコードに厳密な初期化を強制的に課さない。javac
動機:現状の課題
デフォルト値の表裏
Java は未初期化メモリへの参照を防ぐ安全網としてデフォルト値(数値 0、
false、null)を設けています。しかし、これは正当なデータと誤って解釈されやすいことがあります。
- 例: メソッドが
を受け渡ししてしまい、遠く離れた場所でnull
が発生する。NullPointerException - 問題点: JDK 14 などで例外メッセージは改善されましたが、初期化のバグそのものを修正することはできません。
final フィールドの不一致
-
非 strict な final フィールド: クラス/インスタンスが「ラヴラル(初期化途中)」状態の間、異なる値が観測されることがあります。
-
実務上のバグ例:
class App { // [1] 現在、appID は未初期化 (デフォルト値 0) public static final long appID = Log.currentPID(); public static void main() { IO.println("App[" + appID + "] has started"); // ... Log.log("Completed 'main'"); } } class Log { // [2] App.class が初期化される時、Log クラスも初期化する private static final String prefix = "App[" + App.appID + "]: "; // [3] public static void log(String msg) { IO.println(prefix + msg); } public static long currentPID() { return ProcessHandle.current().pid(); // [5] 現在のプロセス ID を返す } }実行結果:
(正しく初期化後)App[96052] has started
(初期化途中のデフォルト値 0 が使われた)App[0]: Completed 'main'
原因:
の呼び出しがLog.currentPID()
クラスのクラス初期化をトリガーする [2]。Log- その際、
はまだ初期化されていないため、デフォルト値App.appID
が読み込まれる [3]。0
からApp
を呼び出し、正しい ID を取得してcurrentPID()
に設定するまでの間に遅れが生じる [4][5][6]。appID
解決策:厳密なフィールド初期化 (Strict Field Initialization)
- コンパイラが指定したフィールドは、JVM で明示的に初期化するように保証される。
- 予期せぬデフォルト値と整合性のない
フィールドを持つことが不可能になる。final - 直感的に期待する「参照前に同様の値が観測される」性質を、JVM レベルで強制する。
厳密なフィールド初期化による新しい機能の基盤
この機能は以下の 2 つの新たな Java 言語機能の実現を可能にする:
- Value クラス:
- インスタンスが同一性を欠き変更されない新しいクラスの基盤。
なインスタンスフィールドは、常に同じ値を観測しなければならない。final
- Null 制限付きフィールド (Null-restricted fields):
- 決して
を保存できないフィールド。null - デフォルト値として
を使用せず、読み取る前に明示的に非null
の値で初期化する必要がある。null
- 決して
注意: これはプレビュー版 VM 機能です。実行時には
を指定する必要があります。--enable-previewjava --enable-preview Main
実装の詳細と仕組み
コンパイラとの連携
- コンパイラ(
など)がソースコードに基づいて、厳密な初期化の対象となるフィールドを選択し、クラスファイルにjavac
(0x0800) フラグを設定する。ACC_STRICT_INIT - Value クラスの場合は、すべてのフィールドを自動的に
としてマークします。ACC_STRICT_INIT
今日までのクラス初期化と拡張
JVM は常にクラスを初期化しますが、厳密な初期化には追加の追跡が必要です。
クラスの状態
- 未初期化: ロードはしたが、初期化未開始。
- ラヴラル (Laeral): 現在初期化中(発展途中)。
- 初期化済み: 成功して完了。
- エラー: 失敗。
静的フィールドの厳密な初期化ルール:
- ラヴラル状態にあるクラス内で、未設定の
フィールドを読み取ろうとすると例外スロー。ACC_STRICT_INIT - ラヴラル状態で未設定の厳密に初期化されたフィールドを書き換えようとすると例外スロー。
- 初期化完了直前に、すべての厳密な静的フィールドが設定されていることを確認し、そうでなければ例外をスロー。
インスタンスの状態と初期化順序
JVM はインスタンスも「早期ラヴラル」と「晚期ラヴラル」の状態を持ちます。
- 早期ラヴラル:
で作成直後。フィールドは書き込み可能だが、読み込みやメソッド呼び出しは制限されている。new - 再帰的な初期化: コンストラクタチェーン (
,super()
) を通じて処理が進む。this() - 晚期ラヴラル:
が完了。全てのフィールドが設定済み。完全に使える状態へ移行。Object::<init>
インスタンスフィールドの厳密な初期化ルール:
- ライバル型状態(早期ラヴラル)において、未設定リストを維持する。
によって未設定リストから該当フィールドを削除していく。putfield
は、未設定リストに存在する場合の読み込みは禁止(例外)。getfield
(メソッド呼び出し) は、スーパークラスへの呼び出しの場合のみ「全ての厳密な初期化フィールドが設定済み」であることを要求する。invokespecial
型状態と検証器の拡張
- StackMapTable アтриビュートに、未設定フィールドのリストを含む新しいフレームタイプ
を追加。early_larval_frameearly_larval_frame { u1 frame_type = EARLY_LARVAL; /* 246 */ u2 number_of_unset_fields; u2 unset_fields[number_of_unset_fields]; // NameAndType 定数の配列 } - 検証器が静的に、早期ラヴラル中に発生する
やgetfield
を制限し、安全性を保証します。invokespecial
Deep Reflection とシリアライゼーションの制約
Deep Reflection の制限
- JDK 26 からは、深層反射による最終フィールドの変更は警告レベルになり、将来的には明示的有効化が必要になります。
- 厳密に初期化された
フィールドへの反射変更 (final
,setAccessible
) は許可されません。set- これらのフィールドは不変としてカテゴリー化されており、例外がスローされます。
カスタムシリアライゼーションの必須化
- シリアライズ/デシリアライズプロセスは
メソッドをスキップします(反射的な構築)。<init> - これは厳密な初期化制約へのバイパスとなり、厳密に初期化したインスタンスフィールドを持つクラスでは
を直接使用できないようになりました。ObjectInputStream - 回避策: 代替オブジェクトを返す
およびwriteReplace
メソッドを実装する必要があります。readResolve
利用環境とリスク
サポートされる変更点
| 変更内容 | 詳細 |
|---|---|
| java.lang.reflect.Field | メソッド追加、フラグの反映 |
| java.lang.classfile API | フラグおよび エントリのサポート |
| javap ツール | 上記フラグとエントリを可視化します |
| AsmTools ライブラリ | 同様にサポートを追加しています |
代替案の是非
- ConstantValue 属性: 静的基本型や文字列のみならず、コンストラクタ引数や動的な計算に基づく初期化が必要な場合は不十分です。
- JDK 21 の警告: 「this は完全に初期化されていない」という警告は有用ですが、無視可能なため、コード規約の強制力に欠けます。厳密な初期化はその制限を回避します。
- Lazy 定数: 初期化タイミングを遅らせる場合などに有効ですが、複雑性を引き継ぎます。
リスクと仮定
- コストとのトレードオフ: 新しい機能は高価です。Value クラスや Null 制限付きフィールドなどの新しい言語機能の成功が、このコスト正当化の鍵となります。
- 既存ツールリスク:
フラグ (ACC_STRICT_INIT
) の誤設定は理論上ありますが、バージョン番号(XX.65535)の違いにより混乱は起きにくいです。0x0800