Java Valhalla: Twelve Years to Delete One Assumption
Project Valhalla finally lands in JDK 28 as a preview. The story of how a decade of work boils down to one idea: let objects opt out of identity.

Years ago I had to make a Java service crunch millions of little coordinate objects fast. The profiler kept pointing at the same thing: cache misses. Not my code. The shape of my data.
So I did what every Java programmer eventually does when speed matters more than dignity. I deleted my nice Point class and replaced it with two raw int[] arrays, one for x and one for y. It was faster. It was also a swamp. Mix up an index once and you corrupt everything silently.
That swamp is exactly the thing Project Valhalla has spent twelve years trying to drain.
The story in one sentence
On June 15, Oracle engineer Lois Foltan confirmed it: JEP 401, Value Classes and Objects, is being merged into the main OpenJDK repository, targeting JDK 28. The integration is so large that other committers were asked to hold off on big commits while it lands. The pull request is over 197,000 lines across 1,816 files.
What does all of that buy you? One word in your source code.
value class Point {
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];
The only difference from a normal class is the value modifier. But the difference in memory is the whole point.
What “value” actually changes
Since 1995, every Java object except the eight primitives has had identity. When you write Point p = new Point(1, 2), p is not a point. It is a coat-check number. The real object sits somewhere on the heap, and every field read is a hop through that pointer.
For one object, that hop is free. For a million, it is a million slips of paper pointing at a million boxes scattered across the warehouse. Brian Goetz calls that layout “fluffy.” The CPU reads memory in 64-byte cache lines, and a fluffy layout wastes most of every line on pointer-chasing and per-object headers.
A value class has no identity. There are no “two different” copies of new Point(1,2), the same way there aren’t two different 4s. Give up identity, and the JVM is suddenly free to store the values themselves, side by side, with no headers and no pointers.
When you iterate now, the processor reads sequentially. Each cache line drags in several whole points at once. On data-heavy loops, that is a difference of multiples, not percentages. And you kept the class: the name, the constructor, the validation, the methods. You got the density of a primitive and the readability of a class.
Why a JVM internals deep-dive hit the front page
Because of the joke. For years the community has said we will all reach Valhalla, the Norse afterlife one, before the project ships. Goetz himself predicted that the “they’ll never ship it” crowd would instantly pivot to “but they didn’t ship the most important part.” He is right, and that tension is half the thread.
It also hit because the honest version of the headline is a half-truth. “Valhalla rolls into JDK 28” sounds like arrival. It is preview, disabled by default, and only the first phase. Non-nullable types, specialized generics, 128-bit encodings, flat ArrayList over value types: none of that is in 28. JDK 28 isn’t even an LTS, so most companies meet a stabilized Valhalla around JDK 29 in late 2027.
What HN is actually arguing about
The sharpest dissent comes from rf15, reacting to the article calling the old two-projection model “mentally heavy”:
“No it isn’t! … There is no reason to reduce the optional safety guarantees you can offer with the excuse of ‘too mentally taxing’.”
That is the real fight. The team killed the Point.val / Point.ref dualism to keep the language simple, and split non-nullability into a separate future JEP. Some people wanted that null-safety baked in now, and read the simplification as a retreat.
layer8 found a subtler trap: for value objects, == now compares all fields recursively, basically a memcmp. That can expose internal representation that equals would have hidden.
And tomaytotomato defended the whole thing with a parenting metaphor: Java was raised by loving parents (Sun), then neglected in a garage by an evil guardian (Oracle), and has been playing catch-up ever since. Harsh, affectionate, and very HN.
My favorite comment wasn’t an argument at all. smallnix noticed the article’s [IMAGE: ...] placeholder and deadpanned that the Point[] in the prompt had crashed the author’s LLM image generator. A post about flattening arrays, undone by an array in a string.
Should you read the original?
| Read it if… | Skip it if… |
|---|---|
You write Java and want the real model behind value |
You only care once it’s the LTS default (wait for JDK 29) |
| You’ve bounced off Valhalla’s naming history before | You already follow the OpenJDK mailing lists |
| You do data, ML, gamedev, or finance work on the JVM | You don’t control allocation hotspots in your code |
The piece is the clearest single map of a decade of dead prototypes I’ve seen: Q World, L World, value types, inline classes, primitive classes, and finally just value classes.
Here is the part that stuck with me. Twelve years wasn’t twelve years of writing code. It was twelve years of throwing ideas away until one was left that could actually be maintained. The shipping artifact is one keyword. The work was deciding which assumption, true since 1995, they were finally allowed to delete.
I still have that old service with its two int[] arrays somewhere. One day I get to delete the swamp and write value class instead. I just have to wait for the LTS, like everyone else who’s been waiting for Valhalla.
Discussion on Hacker News · Source: jvm-weekly.com · Submitted by philonoist
Related posts
DuckDB Is Fast Because It Skips the Part Everyone Forgets
A deep dive into DuckDB internals: why an in-process SQL engine that skips the network beats clusters that cost millions, and what HN thinks about it.
Python 3.15's Most Useful Changes Flew Under the Radar
Python 3.15 ships lazy imports and a Tachyon profiler, but the quietly useful additions are TaskGroup.cancel(), fixed context manager decorators, and…
XS: A 2.9 MB Binary That Is an Entire Programming Toolchain
XS packs compiler, debugger, formatter, package manager, and six backends into one 2.9 MB binary that runs on Linux, macOS, ESP32, and more.
zerostack: the coding agent that runs on 8MB, not 8GB
A solo Rust developer shipped a full-featured coding agent with an 8MB RAM footprint. HN argues about whether that even matters.