Project Valhalla and JDK 28: Bringing Value Classes to Java

Project Valhalla is arriving in JDK 28 via JEP 401: Value Classes and Objects. This update allows developers to define classes that "code like a class but work like an int," enabling the JVM to store data densely in memory and reducing the overhead associated with object identity, pointers, and object headers.

The Core Problem: "Fluffy" Memory Layout

Java's fundamental design—where almost everything is a reference type—creates a "fluffy" memory layout. When a programmer creates an array of objects, the JVM does not store the objects contiguously; instead, it stores an array of pointers (references) to objects scattered across the heap.

This architecture causes two primary performance bottlenecks:

  1. Pointer Indirection: Every access to an object field requires a "hop" through a pointer, increasing the risk of cache misses.
  2. Object Overhead: Every object on the heap carries a header (metadata for type and synchronization), which consumes significant memory.

Modern CPUs are orders of magnitude faster than main memory, making locality of reference critical. When data is stored densely, a single CPU cache line (typically 64 bytes) can load multiple values at once. The current reference-heavy model often forces the CPU to wait for memory fetches, severely limiting performance in data-intensive applications.

JEP 401: Value Classes and Value Objects

JDK 28 introduces the value modifier, allowing the creation of value classes. Instances of these classes are value objects, which are defined by their state rather than a unique identity.

Key Characteristics of Value Classes

  • No Identity: Two value objects with the same field values are considered substitutable. They do not have a unique memory address that defines them.
  • Implicitly Final: All instance fields in a value class are implicitly final.
  • No Synchronization: Because they lack identity, you cannot synchronize on a value object; attempting to do so throws an IdentityException.
  • Reference Type Status: In JDK 28, value classes remain reference types, meaning they can still be null. Non-nullable value types are planned for a future JEP.

Changes to Equality

The meaning of the == operator changes for value objects. Instead of comparing memory addresses (identity), == now checks for substitutability—whether both values belong to the same class and have the same field values. For business-logic equality, equals() remains the recommended method.

Performance Mechanisms: Scalarization and Flattening

By removing the requirement for object identity, the JVM can optimize value objects using two primary techniques:

1. Scalarization

This JIT compiler optimization "breaks down" a value object into its constituent fields. Instead of passing a pointer to an object, the JVM passes the raw fields (e.g., three bytes for a Color object) directly in CPU registers. This effectively makes the object allocation "free" and removes work from the Garbage Collector (GC).

2. Heap Flattening

The JVM can encode a value object's data directly into a field or an array cell without a pointer. This creates a dense memory layout where an array of value objects is stored as a contiguous block of data, similar to a primitive array (e.g., int[]).

The Atomicity Constraint: For flattening to occur, the data must typically be readable and writable atomically to prevent "tearing" during concurrent access. On current platforms, this generally limits flattening to objects that fit within 64 bits (including the null flag). Larger objects may remain on the heap as ordinary objects unless future updates (such as 128-bit encodings or null-restricted types) are implemented.

The Road to Specialized Generics

While JDK 28 brings value classes, it does not yet solve the problem of type erasure in generics. Currently, a List<Point> still stores references to Point objects because the type parameter T erases to Object at runtime, forcing the value object to be materialized on the heap.

Project Valhalla's long-term plan to fix this involves two phases:

  • Universal Generics: A language-level change allowing type variables to cover value types (introducing new compiler warnings to prepare APIs for specialization).
  • Specialized Generics: Future JVM extensions that will generate specialized class layouts for concrete type arguments, finally allowing ArrayList<Point> to be as efficient as Point[].

Summary of JDK 28 Delivery

Feature Status in JDK 28 Note
value modifier Preview Enables declaration of value classes/records
Scalarization Preview JIT optimization to eliminate allocations
Heap Flattening Preview Dense storage for small value objects
Boxed Primitives Preview Integer, Long, etc., become value classes
Non-nullable Types Not Included Planned for a future JEP
Specialized Generics Not Included Planned for a future release

Community Perspectives and Technical Trade-offs

Technical discussions surrounding the release highlight several critical points:

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

"If I have a function that has a value x that erases to java.lang.Object... it used to be safe to check for nullity and then synchronize on the object. This is no longer safe: This can now throw IdentityException into your face." — @leiroigh

Critics have noted that the 64-bit limit for atomic flattening may restrict the benefits to very small data types. Others have argued that the decision to keep value classes as nullable reference types in the first phase simplifies the user model but lowers the performance ceiling compared to a strict "primitive class" model. However, proponents argue that this incremental approach is the necessity of maintaining Java's massive existing ecosystem and backward compatibility.

Sources