Why architecture matters here
Valhalla is OpenJDK's long-running attempt to remove a cost baked into Java's object model: every object has identity, identity needs a header, and a header forces indirection. The project's answer is the value class — a class that declares identity unnecessary, so the VM may flatten its instances into fields, arrays and registers. This is in-progress work whose surface syntax has changed repeatedly, so read the mechanisms below rather than the spelling.
The identity tax and what it buys
Every ordinary Java object carries identity: it occupies exactly one address, distinguishable from a bit-for-bit identical twin. HotSpot pays for that with a per-instance header — a mark word plus a class pointer, roughly 8 to 16 bytes depending on the JVM and whether compact headers are in play. A Point of two ints ends up in a padded cell two to three times its 8-byte payload.
The header is the machinery identity runs on: the mark word memoises the identity hash code, records lock state on synchronization, and holds a forwarding pointer during copying collection. Because those bits are per-instance, the JVM may not silently duplicate the object.
That prohibition, not the byte count, is the real cost. Identity forces an object to be referenced rather than embedded: every field and array element of that type is a pointer, and every read is a dependent load into a cache line no prefetcher could predict. Iterating a long[] is a streaming read; iterating an equivalent Point[] is a random walk through the heap.
Codes like a class, works like an int
Primitives are flat and identity-free but cannot have methods, implement interfaces, be a type argument, or be null; references get the abstraction and pay the indirection. Valhalla collapses that split. A value class keeps constructors, methods, private fields, interface implementation, nominal typing and encapsulation, and surrenders one capability: identity. Its instances are defined entirely by their field values, which licenses the VM to copy them, split them across registers and embed them in whatever holds them. Records are not this — a record instance still has a header — but the two compose, and value record Point(int x, int y) {} is the idiom to expect.
The architecture: every change explained
Read the diagram top to bottom. The top row is the core move: a classic object (header, reference, heap cell) becomes a value class whose data can be flattened, which lets it behave like a primitive. The middle rows are the consequences and the constraints — layout, nullability, generic specialisation, migration of existing types, and the JIT's existing escape analysis becoming easier. The bottom row is why anyone cares. Each of these is unpacked in its own section below.
What removing identity actually changes
Equality. == between value class references compares component state, recursively: primitives by value, nested values by state, ordinary reference fields still by identity. Two independently constructed equal Points are ==, and System.identityHashCode stops being a unique instance token — which makes IdentityHashMap and WeakHashMap meaningless when keyed on values. Weak, soft and phantom references likewise say nothing.
Locking. synchronized (point) has no monitor to acquire, and the design rejects it. That is a live hazard, not a hypothetical: HotSpot already ships -XX:DiagnoseSyncOnValueBasedClasses to surface code that locks on an Integer. See the synchronized deep-dive.
Tearing. A flattened non-volatile field or array element wider than the hardware loads and stores atomically can be seen torn under a data race: x from one write and y from another, a state no constructor produced. Identity objects were protected by the Java Memory Model's final-field publication rules, which work because publishing a reference is one atomic store; a flat value has no reference to publish. Atomicity therefore becomes explicit: a class that must never tear gives up flattening beyond the atomic width.
Null restriction, defaults, and two shapes of one type
Null is the second obstacle. A reference can be null because address zero is a spare bit pattern; a Point flattened into 64 bits has none, since every combination is a legal Point. A nullable slot needs either a flag byte, which wrecks the layout and pushes the field past the atomic width, or an indirection.
Hence two shapes of one class: the ordinary nullable reference type, and the null-restricted type, spelled Point! in current drafts, which excludes null and flattens cleanly. Null restriction drags in a default, because new Point![100] must fill a hundred slots before any constructor runs; it is therefore tied to the class being implicitly constructible. LocalDate, where all-zero fields denote an invalid date, can be a good value class while declining the zero default. Identity-freeness, null-restriction and atomicity are three independent knobs; how much flattening you get depends on which you turned.
value record Point(int x, int y) {}
class Body {
Point maybe; // nullable: needs a null flag, or an indirection
Point! here; // null-restricted: flattens into Body's layout
}
Point[] refs = new Point[1_000_000]; // array of references
Point![] flat = new Point![1_000_000]; // one contiguous blockWhen the JVM can actually flatten
Size and atomicity. A payload fitting one or two machine words flattens happily. Beyond that a non-atomic layout risks tearing and an atomic one needs a lock or a wide compare-and-swap, so large or atomic values fall back to a heap buffer plus a reference: identity-free, but without the layout win. The same rule blocks a wide volatile field.
Class loading order. To lay out class Body { Point! p; } the VM needs Point's size while linking Body, yet loading is lazy. Valhalla adds a classfile attribute — most recently a loadable-descriptors list — naming value classes to resolve eagerly; if that fails the field degrades to a reference and the program still runs.
Arrays. Flat arrays need a null-restricted component type; a nullable Point[] stays an array of references.
A second, independent flattening lives in the JIT calling convention: C2 can scalarise a value argument or return into registers, so a method taking a Point receives two ints and neither side allocates — even when nothing in the heap is flat. Most zero-allocation claims for small value classes come from this, and it applies to nullable value types too.
End-to-end value type use
Trace a use. You write `value class Point { int x; int y; }`. Use it: `Point p = new Point(1, 2);`.
Under classical Java that is a heap allocation — header plus 8 bytes of data, padded — with a reference held by the caller. Under Valhalla p is 8 bytes the JIT can keep in registers: no heap cell, no header, no GC, no indirection.
Now Point[] points = new Point[1000000]. Classical: array of a million references + a million heap objects. Total memory ~28 MB, cache-unfriendly.
With Valhalla: a single 8 MB contiguous array of flattened Points. Iteration becomes streaming reads with no pointer chasing, so a scan that previously took one cache miss per element takes roughly one per eight; the speedup is whatever your loop was paying for those misses.
Migration path: java.util.Optional is already documented as a value-based class. Its descriptor does not change, so compiled callers keep working; what changes is that == compares state rather than addresses.
Generics over primitives and the specialization problem
Generics are erased: ArrayList<Point> compiles to an Object[]. Even a flat Point is re-boxed on the way in, because the slot type is a reference, so List<Point> would chase pointers exactly as List<Integer> does. Two problems stack. A type variable currently ranges only over reference types, so List<int> does not parse; the direction is universal generics, permitting primitives and null-restricted values as type arguments with defined answers for T t = null and new T[n]. And syntax alone buys nothing, because specialising the layout means the VM instantiating a distinct ArrayList with a flat backing store — a parametric-VM capability costing class-loading and code-cache footprint, constrained by ArrayList staying binary compatible with a decade of compiled call sites.
The staging follows: identity removal and flat fields and arrays first, universal generics next, specialised layouts last. So you get the win in your own data structures and arrays, not automatically inside java.util. Code that cares still reaches for int[], a flat Point![], or the Vector API.
Migrating existing types without breaking the world
Migration is conceivable because converting an identity class into a value class does not change its descriptor: Ljava/util/Optional; stays Ljava/util/Optional;. Compiled clients keep linking and simply start receiving instances the VM may copy. Earlier designs gave flattenable types a distinct descriptor, which would have made every migration a breaking change; consolidating on one reference descriptor is what put in-place migration on the table.
The JDK has been staging for years: the value-based class language in the javadoc for Integer, Double, Optional, LocalDate and the List.of family is a pre-declaration not to depend on identity. What breaks when a type flips: synchronizing on an instance; reading == as "same object" — Integer is the cautionary case, true inside the small-value cache and false outside it, becoming consistently state-based; identity-keyed structures such as IdentityHashMap; interning caches like the one behind Integer.valueOf, which exist to preserve identity; and reflection or serialization paths that build instances without running a constructor. See the Optional article.
Where the performance actually comes from
Locality is the largest single win, and it scales with array size: a 64-byte cache line holds eight flat 8-byte Points, or sixteen references each pointing somewhere else.
Allocation rate. Intermediates that are a TLAB bump plus a header today become registers when scalarised, and young-generation cost tracks allocation rate. Escape analysis already chases this, but it is a JIT proof obligation that fails silently when the value is stored to a field, returned through a megamorphic call site, or not inlined. Value classes make the same optimisation declarative.
The costs. Pass-by-value is a copy: a 64-byte value threaded through a deep call chain copies 64 bytes per frame where a pointer copied eight, so large values are worse as values than as objects. Buffered values still allocate, and every boundary into erased generic code, an Object field or a collection re-imposes the boxing you removed.
Status: an in-progress project with moving syntax
Valhalla has been an OpenJDK project since 2014 and is, as of this writing, unfinished; nothing here is a shipped API. The terminology alone has turned over several times — value types, then inline classes, then primitive classes with explicit reference and value projections, now value classes with null-restriction specified separately. Each rename tracked a real design change, most consequentially the consolidation onto one reference descriptor.
Stable enough to build intuition on: identity is what is being removed; null-restriction and atomicity are separate opt-ins deciding how much flattening you get; migration is descriptor-compatible; generic specialisation is a later phase. Not stable: keyword spelling, null-restriction syntax, which proposal carries which piece, and the release in which any of it is final. Every published target date so far has moved, so this article quotes none. Use the project's early-access builds, expect a preview flag, and confirm any speedup against an allocation profile such as JFR, because a value class that quietly failed a precondition allocates exactly as much as the class it replaced.
== becomes state-based, synchronized is rejected and weak references stop meaning anything. Generic specialisation is a later phase, so collections keep boxing. Learn the mechanism, not the spelling.