☕🪪 Java VALUE CLASSES: why losing IDENTITY can unlock PERFORMANCE #valhalla
☕🪪 Java VALUE CLASSES: why losing IDENTITY can unlock PERFORMANCE #valhalla
Value classes get faster by throwing something away: identity.
🔸 TLDR
“The main optimization advantage of value classes is that we give up identity.”
But what exactly are we giving up?
▪️ Traditional Java objects have a unique identity independent of their state.
▪️ Preserving it often means maintaining a distinct object representation.
▪️ Value objects say: the state is the object.
▪️ That gives the JVM much more freedom to flatten, scalarize and sometimes eliminate allocations entirely.
JEP 401 makes this model a preview feature in Java 28.
So let's unpack identity 👇

🔸 1️⃣ WHAT IS OBJECT IDENTITY?
Answer
Identity is the JVM-visible fact that two objects are distinct even when all their fields are equal. With identity objects, == asks: “Are these references the same object?” Two equivalent objects can therefore still have different identities.
record Point(int x, int y) {} var a = new Point(10, 20); var b = new Point(10, 20); a.equals(b); // true a == b; // false
Code explanation
a and b contain exactly the same data.
But Java traditionally considers them two different objects.
Their state is equal.
Their identity is not.
#ObjectIdentity
🔸 2️⃣ HOW WAS THAT IDENTITY MATERIALIZED?
Answer
Traditionally, new creates a fresh identity object, typically materialized as a heap allocation reached through a reference/pointer, with object-header overhead. HotSpot may remove that allocation, but only when it can prove that identity is never observable.
Point p = new Point(10, 20); // Conceptually: // // p ──────────► +-------------+ // | Object | // | header | // | x = 10 | // | y = 20 | // +-------------+
Code explanation
The reference does more than let us reach the fields.
It also lets the runtime distinguish this particular object from every other object.
That distinct representation has a cost:
▪️ Heap allocation
▪️ Object header
▪️ Pointer dereference
▪️ GC work
▪️ Potentially poorer cache locality
#JavaMemory
🔸 3️⃣ WHY DID JAVA NEED IDENTITY?
Answer
Identity matters when objects are mutable. Two objects can have the same state now but evolve differently later. It also enables identity-sensitive operations such as synchronization and identity-based comparison.
class Counter { int value; } var a = new Counter(); var b = new Counter(); a.value = 10; b.value = 10; a.value++; // changes a only System.out.println(a.value); // 11 System.out.println(b.value); // 10
Code explanation
Before the mutation, a and b looked identical.
But they were not interchangeable.
We need identity to know exactly which Counter we are modifying.
For mutable objects, identity has real semantic meaning.
#JavaObjects
🔸 4️⃣ WHAT HAPPENS WHEN WE GIVE IDENTITY UP?
Answer
A value object is defined only by its state. The JVM no longer has to preserve a unique object location, so it may flatten fields into their container or scalarize components into locals, stack slots or registers. new therefore does not necessarily imply a distinct heap object.
value record Point(int x, int y) {} var a = new Point(10, 20); var b = new Point(10, 20); Objects.hasIdentity(a); // false a == b; // true
Code explanation
There is no observable distinction between two Point(10, 20) values.
So the JVM does not have to preserve:
reference → unique object → fields
It may instead work much more like:
x = 10
y = 20
That freedom is the optimization opportunity.
#ProjectValhalla
💡NB: In JShell, Objects.hasIdentity makes it easy to tell which objects are value objects and which are regular identity objects: (https://inside.java/2025/10/27/try-jep-401-value-classes/ )
🔸 5️⃣ WHAT DO WE LOSE WITHOUT IDENTITY?
Answer
You lose operations that require a unique object. You cannot synchronize on a value object, identical field states are not distinguishable with ==, and identity-dependent APIs such as weak references cannot be used. Value classes therefore suit immutable, interchangeable data.
value record Point(int x, int y) {} var p = new Point(10, 20); synchronized (p) { // ❌ value objects cannot be monitors }
Code explanation
You are making a semantic promise:
“I will never care which physical instance this is.”
So value classes are NOT simply:
normal classes + more performance
They deliberately remove capabilities.
That is why declaring a class value must first be a domain-model decision.
#ValueObjects
🔸 6️⃣ WHY IS LOSING IDENTITY SO INTERESTING FOR PERFORMANCE?
Answer
Because identity-free semantics are a guarantee, not an optimization guess. C2 no longer has to prove that identity is unobservable before scalarizing. This can remove allocations, pointer chasing and GC pressure while improving locality.
value record FourLongs(
long a,
long b,
long c,
long d) {}
static FourLongs bumpA(FourLongs v) {
return new FourLongs(
v.a() + 1,
v.b(),
v.c(),
v.d());
}At source level:
new FourLongs(...) new FourLongs(...) new FourLongs(...) ...
But C2 may effectively work with:
a b c d
instead of materializing a heap object for every transformation.
Code explanation
With an identity class, the optimizer has to prove:
“Nobody will observe the identity of this object.”
With a value class, you already gave the JVM that guarantee.
That is a much stronger starting point.
And JEP 539 adds another important guarantee: value-class fields are strictly initialized before they become observable.
For immutable final fields, that can even make larger flattened representations safe:
value record FourLongs(
long a,
long b,
long c,
long d) {}
record Envelope(FourLongs payload) {}payload is final and strictly initialized.
The JVM knows it will never later be replaced with another value, giving it more freedom to flatten its representation.
#JVMPerformance
🔸 ⚠️ BUT VALUE DOES NOT MEAN “NEVER ALLOCATED”
There is an important subtlety.
interface Fun<T> { T apply(T value); }
Generic types are normally erased.
A value crossing an Object-typed or erased boundary may therefore need to be materialized into a real heap object.
Johan Sjölén's example showed exactly this: a megamorphic erased call caused materialization, while explicitly exposing the value types in the method descriptor reduced allocation from 192 bytes per invocation to zero.
So:
VALUE CLASS
≠
GUARANTEED FLATTENING
It means:
VALUE CLASS
=
MORE FREEDOM FOR THE JVM
#JavaPerformance
🔸 TAKEAWAYS
▪️ Identity means “which object?”, not “what value?”
▪️ Identity is essential for objects whose individual existence matters, especially mutable ones.
▪️ Immutable data often does not need that distinction.
▪️ Giving identity up allows the JVM to represent values without necessarily preserving a unique heap object.
▪️ That opens the door to FLATTENING, SCALARIZATION, better locality and fewer allocations.
▪️ JEP 539's strict initialization strengthens the guarantees needed for these representations.
▪️ But optimizations are never guaranteed: Object, generic and other abstraction boundaries can still force materialization.
▪️ So don't “value all the classes.”
Use a value class when the domain says:
these instances are immutable and interchangeable.
Performance is the bonus. 🚀
#Java #Java28 #JDK28 #ProjectValhalla #ValueClasses #ValueObjects #JEP401 #JEP539 #JVM #JVMPerformance #JavaPerformance #OpenJDK #SoftwareEngineering
Go further with Java certification:
Java👇
Spring👇
SpringBook👇
JavaFullstackBook👇
