Project Valhalla 與 JDK 28:值類別與物件解析

Project Valhalla 隨著 JEP 401: Value Classes and Objects 的到來,正為 Java 的記憶體模型帶來根本性的變革,其目標指向 JDK 28。其核心目標是實現「程式碼寫起來像類別,運作起來像 int」的類型,讓開發者在維持高階抽象的同時,也能獲得原始型別(primitive types)的記憶體密度與效能特性。

記憶體問題:「蓬鬆」與「密集」佈局的對比

Java 傳統的物件模型是「蓬鬆」的,這意味著其特徵是指標間接引用(pointer indirection)與記憶體膨脹。在標準 Java 中,每個非原始型別的物件都是引用型別;變數並不持有物件本身,而是指向堆積(heap)上某個位置的指標。這種架構引入了兩個主要的效能瓶頸:

  1. 指標間接引用: 存取欄位需要透過指標進行「跳躍」,這經常導致快取缺失(cache misses)。現代 CPU 的速度遠快於主記憶體,因此參考局部性(locality of reference)對於效能至關重要。
  2. 物件開銷: 堆積上的每個物件都帶有一個標頭(header,包含型別與同步的元數據),這會為每個實例增加數個位元組的開銷。一百萬個物件的陣列實際上是一百萬個指向堆積上分散各處的「盒子」的指標。

雖然 JVM 使用 escape analysis 來優化部分這類配置,但這個過程既脆弱且不可預測。微小的程式碼變動都可能導致 JIT 編譯器停止對物件進行純量化(scalarizing),進而導致效能突然退化。

JEP 401: Value Classes 與 Value Objects

作為 JDK 28 的預覽功能,JEP 401 允許使用 value 修飾符來宣告 value classes

Value Classes 的關鍵特性

  • 無身份(No Identity): 與標準物件不同,value objects 沒有唯一的身份。兩個具有相同欄位值的不同實例被視為可替換的。
  • 隱含式 Final: value class 中的所有實例欄位都是隱含式 final 的。
  • 無同步機制: 因為缺乏身份,你無法對 value objects 進行同步(synchronize);嘗試這樣做會拋出 IdentityException
  • 引用型別: 至關重要的是,在 JDK 28 模型中,value classes 仍為引用型別,且仍然可以為 null。不可為 null 的型別計畫於未來的獨立 JEP 中推出。

相等的改變

對於 value objects,== 運算子改變了含義。它不再是比較記憶體位址(身份),== 現在檢查的是可替換性——即兩個值是否屬於同一個類別,且其欄位值是否遞迴地相同。建議開發者繼續使用 .equals() 進行邏輯資料比較,因為 == 檢查的是內部狀態。

效能機制:純量化與扁平化

Project Valhalla 透過兩個主要的 JVM 優化手段來實現效能提升:

純量化 (Scalarization)

這項 JIT 編譯器技術將 value object 拆解為其構成的欄位。JVM 不再傳遞指向物件的指標,而是傳遞原始欄位值(例如,一個 Color 物件傳遞三個位元組)。這消除了配置與垃圾回收的開銷,且與 escape analysis 不同,它更具可預測性,並能跨越方法邊界運作。

扁平化 (Heap Flattening)

扁平化允許 JVM 直接將物件的資料寫入欄位或陣列單元中,而不需要指標。這會建立一個密集的記憶體佈局,讓資料並排排列,從極大化化快取行(cache line)的效率。

技術限制: 為了防止在並行存取時發生「撕裂」(tearing),扁平化資料必須是可原子性地讀寫的。目前,這通常限制扁平化對象於符合 64 位元(包含 null 標記)以內的類別。較大的類別可能需要未來的 128 位元編碼或引入「null-restricted types」來進行扁平化。

對 Java 生態系的影響

原始型別裝箱 (Primitive Boxing)

在 Valhalla 預覽期間,像 Integer, Long, 和 Double 這樣的包裝類別(wrapper classes)將成為 value classes。由於它們不再具有身份,JVM 可以對它們進行純量化與扁平化,從而大幅降低裝箱的開銷,並使 Integer[] 的效率接近於 int[]

泛型挑戰

對於泛型集合(例如 ArrayList<Point>)的完整效能提升,並未包含在 JDK 28 中。由於 Java 使用型別擦除(type erasure),泛型型別參數會被擦除為 Object,這迫使 value objects 必須在堆積上實體化。解決此問題的藍圖包含兩個階段:

  1. 通用泛型 (Universal Generics): 一項語言層級的變動,允許型別變數涵蓋 value types(這會引入關於「null pollution」的新編譯器警告)。
  2. 專用泛型 (Specialized Generics): 未來的 JVM 擴展,將為具體的型別參數生成專用的類別佈局,從而實現真正的扁平化泛型集合。

JDK 28 交付內容摘要

| Feature | Status in JDK 28 | Note | | :--- | :--- | :--- | :--- | | value modifier | Preview | Enables value classes and records | | Scalarization/Flattening | Included | For qualifying small value classes | | Primitive Wrapper Migration | Included | Integer, etc., become value classes | | Null-Restricted Types | Not Included | Planned for future JEP | | Specialized Generics | Not Included | Future research/release |

社群觀點與技術權衡

關於向 value types 轉型的過程,引發了關於 Java 型別系統權衡的討論:

"== for value classes will basically be like memcmp(). That is a bit unfortunate, as it breaks encapsulation, exposing implementation details." — @layer8

其他貢獻者也指出,舊有程式碼中可能存在相容性問題,因為舊有程式碼可能依賴於對物件進行同步,而這些物件現在可能被擦除為 value types,進而可能拋出 IdentityException

儘管開發週期漫長——跨越了十多年並經歷了多個被捨棄的原型(例如「Q World」與「L World」模型)——但共識是,Valhalla 值得代表一個基礎性的變革。透過讓開發者可以選擇不使用物件身份,Java 將其程式模型與現代硬體效能需求對齊齊了。

Sources