Engineering

Project Valhalla and Value Types

Codes like a class, works like an int — and why the JVM needed a decade to get there

Learning Objectives

By the end of this module you will be able to:

  • Explain what value types provide that reference types cannot, using memory layout diagrams.
  • Describe how object header elimination and heap flattening reduce GC pressure.
  • Explain why value types cannot have identity semantics and what programming patterns this excludes.
  • Connect Valhalla's value types to analogous concepts in Rust and C#.
  • Explain how generic specialization with value types addresses a long-standing erasure limitation.
  • Assess the current readiness of Valhalla features for production use.

Core Concepts

The object tax

Every Java object allocated on the heap carries a header. On a modern 64-bit HotSpot JVM, that header is 12–16 bytes: a mark word used for locking, GC state, and identity hash codes, plus a compressed class pointer. That overhead exists regardless of how small the object's actual payload is.

A Point record with two int fields needs 8 bytes of data. Wrapped in a Java object, it occupies at minimum 20 bytes (8 payload + 12 header), typically padded to 24 bytes for alignment. Stored in an array, you pay the header for every element, and each slot in the array is a pointer (4–8 bytes) that redirects to a separately allocated Point on the heap. Cache efficiency and GC throughput both suffer.

JEP 401 names the goal plainly: eliminate the header for types that don't need it.

What makes a value class

A value class is declared with the value modifier:

value class Point {
    int x;
    int y;
}

Two constraints follow directly from the design:

All instance fields must be final. This is not a style recommendation — it is a hard requirement enforced by the JVM. The reason is mechanical: if a value object is flattened inline inside another object or array, there is no unique heap address for it. Mutation would require rewriting every location where the value is embedded, which is impractical. Immutability resolves this by making every value semantically equivalent to a copy of itself.

Value objects have no identity. Two value objects whose fields are equal are equal, including under ==. There is no concept of "this specific instance vs. that other instance with the same data." The language model document describes this as the fundamental trade-off: you give up identity, and in return the JVM gives up the obligation to allocate a distinct heap object.

These two constraints are inseparable. No identity means no synchronized blocks on value objects, no System.identityHashCode, and no reference equality with meaningful results.

Object header elimination

Because value objects have no synchronization requirements and no identity, the 12–16 byte object header is not needed. A Point value object can be represented as two raw int fields — 8 bytes — with nothing more. For arrays of small value objects, this produces a substantial reduction in per-element memory footprint. The Baeldung writeup on reduced object headers provides concrete size comparisons for familiar types.

Heap flattening

Heap flattening is the mechanism by which the JVM encodes a value object's field values directly into the memory of its container — an object field or an array slot — rather than storing a pointer to a separately allocated object.

Fig 1
Reference array Point[] ptr[0] ptr[1] ptr[2] hdr | x=1 | y=2 hdr | x=3 | y=4 hdr | x=5 | y=6 Value array Point[] (flattened) x=1 y=2 x=3 y=4 x=5 y=6
Reference type array vs. value type array layout for Point(x, y)

The top row shows the classical layout: an array of pointers, each pointing to a separately heap-allocated object that includes a header. Data for three points is scattered across at least four distinct memory regions. The bottom row shows the flattened layout: all field values are stored contiguously in a single block, with no headers and no pointers.

This has two compounding effects on performance. First, it directly reduces memory footprint. Second, it dramatically improves CPU cache locality: iterating over an array of flattened value objects reads sequential memory, whereas iterating over a reference array follows a pointer per element, each of which may point to a different cache line.

Array flattening in detail

Array flattening is the specific case of heap flattening applied to arrays. An array of value objects with four int fields per element becomes a single contiguous memory block with interleaved field values. This layout removes per-element pointer overhead and creates opportunities for SIMD optimization at the JIT level.

For numeric workloads — coordinate processing, financial data, signal analysis — the difference in cache behavior between a pointer-indirected array and a flattened array can dominate overall performance.

Scalarization

Heap flattening handles fields and array elements. Scalarization handles method-local scope.

When a value object is used as a local variable or method parameter and the JIT can prove it does not escape the method, the JVM can replace the value object with individual local variables representing its fields — no heap allocation at all. This is similar to escape analysis for identity objects, but more predictable: value types carry a structural guarantee that they have no identity and cannot be aliased by reference, so the JIT can apply scalarization far more aggressively than it can for identity objects where aliasing is always theoretically possible.

Scalarization vs. escape analysis

Traditional escape analysis for identity objects is a probabilistic optimization — the JVM may or may not scalarize depending on callsite complexity and inlining depth. Scalarization for value types is structurally enabled: the absence of identity removes the aliasing problem that makes escape analysis conservative.

GC pressure reduction

Fewer heap allocations means less work for the garbage collector. Value types reduce GC pressure by replacing pointer-chasing object graphs with inline data. For allocation-heavy workloads like parsing, where many short-lived intermediate value objects are created and discarded, flattening and scalarization together can approach an order-of-magnitude reduction in GC frequency. Realistic aggregate scenarios show 40–50% smaller heap footprints compared to the classical Java object model, with 60–70% fewer allocations and 3–5x better data locality for parsing-style workloads.

Generic specialization

Java generics use type erasure: List<String> and List<Integer> compile down to the same bytecode operating on Object. This forces boxing for primitive types — an int must be wrapped in Integer to be stored in a List<Integer>.

JEP 402 and the Valhalla specialization design extend this by allowing the JVM to generate type-specific implementations for generic classes when instantiated with value types or primitives. List<int> would have a distinct runtime implementation that stores raw int values, not boxed Integer objects. This eliminates one of the oldest performance friction points in Java generics and is the feature that completes what erasure left unfinished in 1998.

Generic specialization is not part of JEP 401

JEP 401 (Value Classes and Objects, Preview) delivers value classes and heap flattening. Generic specialization is a separate, later-stage work item in the Valhalla roadmap. The two features are complementary but ship on different schedules.

Null-restricted types

A companion feature tracked under JEP draft 8316779 introduces null-restricted value class types, written with a ! suffix — for example, Point!. A null-restricted type excludes null from its value set. This enables denser heap flattening: without null-restriction, the JVM must represent the possibility of null in the flattened storage. This typically requires an extra bit or byte per slot — a null flag. For large arrays, this overhead is small but not zero. Full flattening efficiency requires null-restricted types, which are a separate preview feature on a separate timeline.

Compare & Contrast

Rust structs

Rust engineers will find Valhalla's value types immediately familiar. A Rust struct stored inline in a Vec<Point> is laid out exactly as a flattened value array in Valhalla: contiguous field values, no pointers, no headers. The semantic rule is the same — structs are copied, not aliased. Rust enforces immutability through the borrow checker; Valhalla enforces it through final fields.

The key difference is timing: Rust had this from day one. Valhalla is retrofitting the same capability into a language and VM built around a reference-object model, which requires solving backward-compatibility problems that Rust never had.

C# structs

C# structs in the CLR are allocated inline in containing objects or on the stack, avoiding heap allocation and GC overhead. The CLR JIT can further promote struct fields into CPU registers. This is structurally identical to Valhalla's scalarization path.

The distinction is that C# structs are mutable by default and carry well-known pitfalls around copying semantics. Valhalla's immutability requirement is a deliberate tightening of the design: all-final fields mean "equality means copies are indistinguishable," which removes the mutable-struct footgun that C# developers routinely hit.

Value classes vs. record classes

Java records (introduced in Java 16) are also immutable and structurally comparable, but they are identity classes — each instance has a unique heap address, carries an object header, and can be compared by reference. A record does not benefit from heap flattening or scalarization. Value classes are the performance-oriented counterpart: where records provide a concise syntax for immutable data, value classes provide a memory-layout guarantee.

Codes like a class, works like an int.

This slogan from the Valhalla design documents captures the intended programming model: value classes look and feel like regular Java classes at the language level, but the JVM is free to treat them as dense data rather than heap objects.

Worked Example

LocalDate as a value class

The inside.java benchmark migrates java.time.LocalDate to a value class with early-access JEP 401 enabled and measures the result.

LocalDate holds three fields: year (int), month (short), day (short) — 8 bytes of data. As an identity object, an array of LocalDate is an array of pointers. Each pointer targets a separately allocated LocalDate on the heap, with its 12–16 byte header.

When LocalDate is declared as a value class:

  1. The object header is eliminated.
  2. An array of LocalDate! (null-restricted) is flattened to a contiguous block of 8-byte entries.
  3. Sequential access to the array reads sequential memory.

The measured result: approximately 3x speedup for memory-access-heavy scenarios. The data payload is unchanged — only the layout is different.

This example matters because LocalDate is not a toy type. It is a widely used standard library class with real-world adoption. If value classes can yield a 3x improvement for it with no change to call sites, the implication for domain model types — Money, Coordinate, RGB, OrderId — is significant.

What the code looks like today (preview)

With --enable-preview on a JDK 25 early-access build:

value class Point {
    final int x;
    final int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

// Array of flattened Points — no per-element pointer, no header
Point[] points = new Point[1_000_000];

The compiler enforces that all fields are final. Attempts to synchronized on a Point or call System.identityHashCode(p) are compile errors. p1 == p2 compares field values, not addresses.

Boundary Conditions

Where value classes cannot replace identity classes

Several common Java patterns depend on object identity and are incompatible with value classes:

  • Synchronization. synchronized (obj) requires identity; the JVM uses the mark word in the object header to track the lock owner. Value objects have no header, no mark word.
  • Mutable state. Fields must be final. Any design that mutates object state after construction cannot be a value class.
  • WeakReference, SoftReference, PhantomReference. These reference types track a specific heap object's identity; value objects do not have stable addresses.
  • Identity hash code. System.identityHashCode returns a stable identifier for a specific heap object. Value objects have no such concept.
  • Inheritance. Value classes cannot be extended. The JVM's ability to flatten a value object depends on knowing its exact layout at compile time.
Not a drop-in replacement

Value classes are a new kind of type, not a performance annotation you apply to existing classes. Migrating an identity class to a value class requires verifying that no code depends on any of the above identity properties.

Flattenability and null

Without null-restriction (Point rather than Point!), the JVM must represent the possibility of null in the flattened storage. This typically requires an extra bit or byte per slot — a null flag. For large arrays, this overhead is small but not zero. Full flattening efficiency requires null-restricted types, which are a separate preview feature on a separate timeline.

Not yet production-ready

Project Valhalla began around 2014 and remains in preview as of 2026. JEP 401 is a preview feature in JDK 25 early-access builds. Preview features require --enable-preview at both compile time and runtime, and their APIs can change between preview iterations with no backward-compatibility guarantee.

The Java preview mechanism sets a high bar: preview features are at least 95% complete before inclusion in a mainline build. They are near-final, not experimental. But "near-final" means they can still change, and production code should not depend on them until they are finalized. The Foreign Function & Memory API went through four preview iterations across Java 19–22 before finalization — Valhalla's scope is larger, and its timeline is likely longer.

Generic specialization, the feature that eliminates boxing for List<int>, is not part of JEP 401 and is not yet available even in early-access form. Full production support with specialization is expected in releases beyond JDK 25.

Key Takeaways

  1. Value classes eliminate the object header (12–16 bytes per instance) and enable the JVM to flatten field values directly into containing objects and arrays, removing pointer indirection and improving cache locality.
  2. No identity is the enabling constraint. Value objects have no unique heap address. Instances with equal field values are equal under ==. This rules out synchronization, weak references, and mutable state — but it is exactly what makes flattening safe.
  3. All instance fields must be final. Immutability is not a stylistic guideline for value classes; it is enforced by the JVM because mutable inline data would require rewriting every location holding a copy.
  4. Scalarization and heap flattening are complementary. Flattening applies to persistent storage (fields, arrays). Scalarization applies to method-local temporaries — the JIT replaces the object with individual register or stack variables when the value does not escape.
  5. Valhalla is still in preview. JEP 401 is available in JDK 25 early-access builds under --enable-preview. Generic specialization is a separate, later-stage deliverable. Do not use preview features in production code.