valhalla: value objects
objects without identity, flattened into arrays. preview in jdk 28.
- chapter
- chapter 18.01
- reading time
- 8 min read
- preview
a value class is a class whose instances have no identity: two of them are == when their fields are equal, you can't lock on them, and the JVM is free to copy them around instead of pointing at them. that freedom is the point: arrays and fields of value objects can be flattened, holding the data inline instead of a pointer to a separate object with a header.
as of october 2026, value objects (JEP 401) are integrated into jdk 28 as a preview, due march 2027. Integer, Optional and the java.time types become value classes under --enable-preview. null-restricted types and specialized generics are still drafts and ideas.
the picture
you know how an ArrayList<Integer> of a million numbers costs about 20 MB while an int[] costs 4? (measured in ArrayList vs LinkedList.) every Integer is a full object: a 12-byte header, the 4-byte value, padding, and a pointer from the list to find it. most of that exists so each Integer can have an identity: so == can tell two 1996s apart, so you could synchronized on one, so it could be null or be the same object in two places.
almost nobody wants any of that from a number. think of writing a phone number on a page versus writing "see the sticky note in drawer 4" and keeping the number on the sticky note. the sticky note is identity: you can point at it from many places and cross it out in one. for a phone number, you just want it written down. valhalla's motto is "codes like a class, works like an int".
what a value class is
preview in 28 one modifier. this is jdk 28 code (preview), adapted from JEP 401's examples, and it doesn't compile on jdk 25 or 27:
// jdk 28, javac --release 28 --enable-preview (JEP 401). illustrative: not runnable here.
value record Point(int x, int y) {}
value class Money {
private long cents; // fields of a value class are implicitly final
public Money(long cents) { this.cents = cents; }
public long cents() { return cents; }
}what changes, per the JEP:
==compares state. two value objects are==if they're the same class and their fields are the same (primitive fields bit for bit, reference fields recursively).new Point(17, 3) == new Point(17, 3)is true.equalsstill exists for "means the same value" when that's looser than "same fields".- no identity operations.
synchronizedon a value object is a compile error, or anIdentityExceptionat run time through anObjectreference.wait/notifythrow. you can't make aWeakReferenceto one.Objects.hasIdentity(o)tells you which kind you have. - the class and its fields are implicitly final. a value object can't change after construction, which is what makes copying it safe.
- it's still an object. it has a class, methods, can implement interfaces, and a variable of a value class type can still be
null. java still has exactly two kinds of values: primitives and references.
and under --enable-preview the JDK itself moves over: Integer, Long and the other wrappers, Optional and its primitive cousins, and most of java.time (LocalDate, Duration, ...) become value classes. String does not.
Integer x = 1996, y = 1996;
IO.println(x == y);flattening
no identity means the JVM never has to keep "this particular object" anywhere. so it can store the data directly where the reference would have been: in an array element, in a field (flattening), or spread across registers in a method (scalarization).
an array of references. every non-null element is a separate 16-byte object with a 12-byte header, somewhere else on the heap. reading one is two loads: the slot, then the object. and a[0] and a[2] are two separate 1996 objects, so == says they're different.
the speedup isn't only memory. one load instead of two, no pointer chasing, data packed in order, so a loop over a flattened Integer[] is a sequential scan, the friendliest thing you can do for a CPU (why that matters: ArrayList vs LinkedList). and the GC has a lot fewer objects to trace.
flattening works best when the variable has the exact value class type. a Point[] or a Point field can be flattened; an Object[] holding points can't, because it might hold anything.
the 64-bit budget
there's a catch that explains a lot of valhalla's roadmap. a flattened value in a field or array element you can write to must be read and written in one go, or another thread could see half an old value and half a new one (a "torn" read). on common hardware that means 64 bits at most. and a nullable reference needs at least one extra bit to say "this is null".
a nullable Long or Point needs 65 bits, one more than the hardware can update atomically, so in a mutable field or array it stays a pointer to an object. small values (Integer, LocalDate, a three-byte Color) fit, flag and all.
so in jdk 28's preview, an Integer[] can flatten and a Long[] probably can't (65 bits, one too many). that's why the other half of valhalla is about telling the JVM more: "this variable is never null" (null-restricted types), or "this class doesn't mind being updated non-atomically" (an option the JEP mentions as possible future work). fields of a value class don't have the limit at all, because nobody can ever observe them changing.
what's next
valhalla has been running since 2014, and it ships in pieces. as of october 2026:
value classes and value records, == by state, and about 30 JDK classes (the wrappers, Optional, the java.time types) migrated under --enable-preview. in the jdk 28 early-access builds now; jdk 28 is due in march 2027.
the order matters: value objects first (identity is the thing that blocks everything else), then nullness (which unlocks more flattening), then generics over primitives (List<int>, which needs both). the vector API is also waiting on value objects: its vectors want to be values, not heap objects.
- a value class has no identity:
==compares fields, no locking, no weak references, immutable. - that lets the JVM flatten value objects into arrays and fields: data inline, no header, no pointer.
- JEP 401 is integrated into jdk 28 as a preview (as of october 2026).
Integer,Optional,LocalDatemigrate under the flag. - flattening a mutable field has a 64-bit budget, null flag included. that's why null-restricted types matter.
- null-restricted types and
List<int>are drafts and design notes, not features yet.