skip to content

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:

java
// 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. equals still exists for "means the same value" when that's looser than "same fields".
  • no identity operations. synchronized on a value object is a compile error, or an IdentityException at run time through an Object reference. wait/notify throw. you can't make a WeakReference to 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.

predict the output
on jdk 28 with --enable-preview, what does this print?
java
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).

Integer[8]headernullnull12 B header199612 B header200612 B header199612 B header712 B header4212 B header1996
bytes per element
4 + 16
loads to read one
2
a[0] == a[2]
false
can hold null
yes

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 same eight elements three ways. switch layouts. today every element is a pointer to an object with its own header. flattened, the data sits in the array. the flattened layout is what JEP 401 says a JVM may do, not a promise: it's the JVM's call, element by element.

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".

Color(byte r, g, b) · 3 × 825 bits · can flatten
Integer · an int33 bits · can flatten
LocalDate · an int and two bytes49 bits · can flatten
Point(int x, int y) · 2 × 3265 bits · too wide
Long · a long65 bits · too wide
Double · a double65 bits · too wide
the object's data the null flag 64 bits: what hardware reads and writes in one go

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.

bits each value needs, plus a null flag, against the 64 bits hardware updates atomically (the dashed line). switch to the draft null-restricted mode to see what dropping the flag buys.

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:

jdk 28, preview

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.

valhalla's pieces and where each one stands, checked against the JEP pages and the project page on openjdk.org (october 2026). click a row for what it is.

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.

keylines
  • 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, LocalDate migrate 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.