Value Classes (JEP 401): When Records Aren’t Flat Enough for the JIT
How value classes differ from records — no identity, no heap header — where they outperform records in tight loops, and the Valhalla mental model in 300 words.
Status check: As of mid-2026, JEP 401: Value Classes and Objects is a preview feature being integrated into the OpenJDK mainline targeting JDK 28, following early-access builds distributed via jdk.java.net/valhalla. It will ship disabled by default and, per Java Language Architect Brian Goetz, is likely to remain in preview even in the next LTS release.
Records solved a real problem: writing a small immutable data carrier used to take a constructor, getters, equals, hashCode, and toString, all by hand, all easy to get subtly wrong. Records collapsed that into one line. What records didn’t solve is a much older, lower-level problem — every record instance is still a regular object, with a regular identity, sitting behind a regular pointer, wearing a regular heap header. For a tight loop processing millions of small immutable values, that header and that pointer indirection are exactly the overhead a record can’t shed. Value classes exist to shed it.
Records Solved One Problem, Not Every Problem
A record gives you field declarations, an implicit constructor, and correct equals/hashCode/toString for free. What a record does not give up is identity: two record Point(int x, int y) instances with identical coordinates are still two distinct objects in memory, each independently allocated, each carrying its own object header, each requiring a pointer dereference to read. An array of a million records is an array of a million pointers to a million separately allocated objects scattered across the heap — exactly the layout that punishes CPU cache locality in numeric or data-heavy loops.
What a Value Class Actually Removes: Identity and the Heap Header
A value class declares that its instances have no identity at all. Instead of asking “is this the same object as that one,” the JVM can only ask “does this have the same field values as that one” — which is precisely what == checks for value objects, since there’s no separate identity left to compare. This single removal is what unlocks everything else: without identity to preserve, the JVM no longer needs to guarantee that two value objects with the same state occupy distinct memory locations, which means it no longer needs to give every instance its own heap header just to track that identity.
Two JIT Superpowers Identity Removal Unlocks
Two optimizations become possible the moment identity is gone. Scalarization lets the JIT decompose a value object into its raw constituent fields directly in registers or on the stack, passing three separate bytes for a Color instead of a pointer to a heap-allocated Color object — in the fully optimized case, no allocation happens at all. Heap flattening lets arrays and fields of value objects store the actual field data inline, contiguously, rather than storing pointers to separately allocated instances scattered across memory. Both optimizations are exactly what a record, with its preserved identity, structurally cannot receive.
| Property | Record | Value Class |
|---|---|---|
| Object identity | Yes — regular reference type | No — Objects.hasIdentity() returns false |
| Heap header per instance | Yes | Eliminated where the JIT can flatten/scalarize |
| Array storage layout | Array of pointers to heap objects | Can be flattened inline, contiguous field data |
| Nullability | Nullable (reference type) | Still nullable — value classes remain reference types in JEP 401 |
== semantics | Reference identity comparison | State-wise equivalence comparison |

Where Value Classes Win in Tight Loops
The gap shows up clearest in code that creates or iterates over huge numbers of small, immutable values — parsing pipelines, numeric simulations, coordinate math, or high-throughput message deserialization. An early-access write-up from the Valhalla team demonstrates this directly with a date-heavy computation: once value objects let the JIT flatten and scalarize the hot path, repeated runs of the same benchmark showed execution time dropping noticeably as the JVM’s optimizations kicked in, compared to the equivalent computation using ordinary identity objects.

The JDK team is migrating some existing platform classes to value classes as this stabilizes — Integer and LocalDate are named candidates — precisely because these are exactly the small, frequently duplicated, identity-irrelevant types where the savings compound the most across the entire ecosystem.
The Valhalla Mental Model in 300 Words
Java has always drawn a hard line between two kinds of types: primitives like int, which are fast, flat, and have no identity, and everything else, which is a reference type with an identity, a heap allocation, and pointer indirection. That line forced an uncomfortable choice for anyone designing a small data type: use a primitive and lose encapsulation, validation, and meaningful naming, or use a class and pay for an object header and a pointer chase on every single instance, even for something as conceptually simple as a coordinate or a currency amount.
Value classes exist to erase that line, not by making objects into primitives, but by identifying which classes never actually needed identity in the first place. A Point, a Money amount, a LocalDate — none of them derive meaning from being “this particular instance in memory.” They derive meaning entirely from their field values. Once a class declares that honestly, the JVM is freed from guaranteeing anything about where that instance lives or whether it’s the only one with those values, and it can flatten, scalarize, and inline accordingly.
The mental model, then, isn’t “value classes are a faster kind of record.” It’s narrower and more honest: identity is a feature some classes need and others were only paying for by default. Value classes let a class opt out of a cost it never used. Records solved the boilerplate problem for data carriers; value classes solve the layout problem underneath them — and the two are meant to be used together, since a record can itself be declared a value class once its use case has no need for identity at all.
Choosing Between Records and Value Classes
Reach for a value class, or a record declared as one, when:
- Instances are small, immutable, and defined entirely by their field values.
- The type appears in large arrays, collections, or tight loops where memory layout affects performance.
- Nothing in the codebase relies on reference identity, synchronization on the instance, or weak references to it.
- You’re prepared to run on a preview JDK build with
--enable-preview, since this isn’t stable or default-on yet.
What We Learned
Records fixed the ergonomics of writing small immutable data types; value classes fix what happens to those types at the memory and JIT level. The difference comes down to one property — identity — and everything else follows from removing it: no mandatory heap header, no forced pointer indirection, and two new JIT optimizations, scalarization and heap flattening, that simply aren’t available to any type that still needs to answer “am I the same object as that one.” JEP 401 remains a preview feature heading toward JDK 28, and its own authors are candid that it’s only the first stage of a much longer effort — but the mental model it introduces is already worth understanding now, since it reframes identity as an opt-in cost rather than something every class pays by default.

