Project Valhalla, Explained: How a Decade of Work Arrives in JDK 28 - JVM Weekly vol. 180
The new JVM Weekly is here... and Ragnarok seems to come, as we finally have Valhalla in the JDK. However, situation is a bit... nuanced.
On June 15, Oracle engineer Lois Foltan confirmed what a good chunk of the industry had stopped believing: JEP 401: Value Classes and Objects will be integrated into the main OpenJDK repository and is targeting JDK 28.
The change is so large that the remaining committers were asked to hold off on bigger commits during the integration. The pull request alone adds over 197 thousand lines of code across 1,816 files.
Before we pop the champagne, though: this is preview, disabled by default, and, as Brian Goetz was quick to cool everyone down, “only the first part of Valhalla.” Goetz added a great observation that the “they’ll never ship it” crowd will now smoothly switch over to “but they didn’t ship the most important part” (and a joke has been going around the community for years that we’ll sooner end up in Valhalla ourselves, the Norse-afterlife one, than the project ships).
You have to earn your own haters.
So this is a good moment to tell the whole story. This issue is one big deep-dive, written on the assumption that you’ve never followed the work on Valhalla before: from the 2014 problem, through the evolution of ideas (a fair number of which ended up in the trash), all the way to what exactly we’ll be getting our hands on in JDK 28. Brew yourself a coffee. I’ve been sitting on this edition for a long time, saving it for exactly this occasion.
1. Introduction - what this is even about
The slogan Valhalla has carried from the start is: “codes like a class, works like an int.” In a single sentence it captures the whole point of the project: we want to write normal, readable classes with methods, constructor validation, and sensible field names, but we want the JVM to be able to treat them as efficiently as primitives.
To understand why this is a problem, you have to go back to Java’s foundation. In this language, with the exception of the eight primitives (int, long, double, boolean, and the rest), everything is a reference type. When you write Point p = new Point(1, 2), the variable p isn’t a point. The variable p is a pointer, a coat-check number: somewhere on the heap sits an object, and you’re holding a slip of paper with its address. Every time you want to read a field, the JVM has to “go to the coat check,” performing a hop through the pointer (pointer indirection).
For a single object, that’s nothing. The problem starts at scale. Every object on the heap has its own header (a dozen-or-so bytes of metadata: among other things, so the JVM knows what type it is and whether anyone is synchronizing on it). Incidentally, this is exactly the problem Project Lilliput has been tackling lately, helping to shrink object header sizes. But header size isn’t everything. Every object has to be allocated, and later garbage collected. And since objects are scattered across the heap, an array of a million Points is in practice a million slips of paper pointing at a million boxes strewn across the whole warehouse.
Brian Goetz, in his “State of Valhalla” documents, calls such a memory layout “fluffy”: puffed up, bloated. What we dream of is a dense layout, one where the data lies side by side.
Why does density matter? Because the hardware changed faster than Java did. In 1995, a memory access cost roughly the same as a CPU operation. Today the CPU is two orders of magnitude faster than main memory, and the whole gap is bridged by the cache. The processor reads memory in chunks called cache lines (usually 64 bytes). If the data lies densely and in order, one such chunk brings in a ton of useful values at once. If we’re hopping across pointers, every access risks a cache miss, and that can be a hundred times slower than a hit. This is locality of reference, and it’s the real stake in this whole game.
“But the JVM has escape analysis,” someone sharp will say. True: the virtual machine can recognize that some object never “escapes” beyond a local fragment of code, and then it doesn’t allocate it at all. From the programmer’s point of view it looks as if the object exists, but in reality its fields get spread out into ordinary variables or CPU registers. In the best case, the cost of allocation and the later cleanup by the garbage collector drops to practically zero.
The trouble is that this optimization is unpredictable and fragile. It works only when the JIT compiler can trace the object’s entire flow with high confidence. But all it takes is for the object to land in a field of another class, get stored in an array, get passed into a more complex method, or appear beyond the boundary of code the JIT can analyze, and the whole trick stops working. The source code stays identical, but the performance behavior can change dramatically.
This is precisely why experienced JVM programmers treat escape analysis as a nice bonus, not a project’s foundation. If an application’s performance depends on whether a particular JIT version manages to apply this optimization, it’s very easy to fall into the trap of hard-to-predict regressions. A minor refactor, a JDK update, or a change in code structure can send objects back onto the heap, and the costs of allocation and garbage-collector work return in full force.
That leaves the brute-force option: give up on objects and encode the data by hand. Instead of a Color class, hold three bytes r, g, b. This isn’t just an academic example. The approach has been used for years in game engines, graphics libraries, image-processing systems, databases, analytics engines, and HPC code, where every byte of memory and every allocation matters. The trouble is that the speed comes at the cost of safety and readability. We lose names, private state, validation, and methods. JEP 401 gives a simple example: a developer working on “raw” color bytes might mistakenly interpret them as BGR instead of RGB, swap red with blue, and quietly corrupt the entire image. A class wouldn’t have allowed it. A bare int? Sure it would.
And it’s exactly this dichotomy, either convenient classes, or fast primitives, that Valhalla is trying to erase.
2. The beginnings - 2014, “six PhDs,” and five prototypes
Officially, Project Valhalla started in 2014. James Gosling described it at the time as “six PhDs tied into a single knot,” and that was no exaggeration. Interestingly, the idea is older than the project itself: Java’s creators wanted value types as early as the first version of the language, but in 1995 they gave up, because the problem was too hard.
The goal was set ambitiously: to restore alignment between the programming model and the performance characteristics of modern hardware. In other words, to let programmers declare their own types that are flat and dense in memory like primitives, but look and behave like normal classes.
Easier said than done. Over the following years the team built five different prototypes, each probing a different aspect of the problem. And this is where the most interesting part of the story begins, because to appreciate Valhalla’s current shape, you have to see how many ideas died along the way.
The early prototypes went in a direction we now call “Q World.” It assumed that the new value types were a fundamentally different beast from objects, with separate type descriptors, separate bytecodes, and separate top types, exactly like primitives. Sounds logical: if they’re supposed to work like int, let them be represented like int. The trouble is that such a separation flooded the entire JVM type system with extra complexity: everything had to be done in two variants.
The breakthrough came with a prototype christened “L World” (roughly around 2019). The name comes from the fact that value types started sharing the same “L carrier” (the L descriptor, the same one the JVM uses for ordinary references) with object references. The team expected such a unification to be too hard, and yet, to their own surprise, it worked without major compromises and incidentally solved a whole pile of problems from the earlier rounds.
L World produced one more fundamental “aha” that shaped everything that came after: the language model and the JVM model don’t have to overlap one hundred percent. L World is the right model for the virtual machine, but you can treat it as a translation target and offer the programmer something more convenient in the language. This separation of layers turned out to be the key to the rest of the project.
That’s also when the plan to split the work into two phases crystallized: first value classes (still called something else at the time, more on that shortly), and only then, specialized generics. We’ll come back to generics in section 6, because that’s a separate, longer treatise.
3. The evolution of ideas - the naming rollercoaster
If you’ve ever tried to read about Valhalla and bounced off a wall of contradictory terms, it’s not your fault. The naming changed several times here, and not cosmetically: behind each name change stood a change in the model. Let’s trace it, because it’s the best illustration of how this feature was designed.
Stage 1: value types: The earliest term. Vague, because it wasn’t yet clear what exactly these things were supposed to be.
Stage 2: inline classes: Around 2019–2020 a distinction settled in that has survived to this day in its essence: classes split into identity classes (the ones with identity, that is, everything we’ve known until now) and the new inline classes (without identity). That’s when the slogan “codes like a class, works like an int” was coined, and the basic constraints were set: inline classes are final by default, their fields are final, you can’t synchronize on them.
Stage 3: “primitive classes” and the two-projection model. And here it gets interesting, because this is exactly the idea that got significantly cut down. In the 2021 “State of Valhalla” documents, Valhalla promised three things: value objects, primitive classes, and specialized generics. The idea for a “primitive class” was that a single type would have two projections: a value variant (flat, never null, behaving like a primitive) and a reference variant (a box that allows null). Across various iterations this was written as Point.val/Point.ref, and later they experimented with the Point! and Point? syntax.
The model was powerful, but also mentally heavy. A programmer would have to juggle two forms of the same type day to day and understand when a conversion between them happens. The team, faithful to the lesson “simplify the model for the user, even at the cost of the performance ceiling,” ultimately dismantled this dualism.
Stage 4 (today): “value classes” and “value objects.” The current JEP 401, authored by Dan Smith (reviewer: Brian Goetz), puts it simply. There’s one new thing: a value class, declared with the value modifier. Its instances are value objects: objects without identity. And (this is key) a value class is still a reference type. The whole tricky business of non-nullability has been split off into a separate, optional JEP (Null-Restricted Value Class Types), which we’ll get to. So instead of one complicated concept we have two simple, orthogonal ones: “does it have identity?” and, separately, for later, “does it allow null?”
Worth remembering, because if you come across an older article (or Baeldung describing “primitive classes” as a separate mechanism), you’re reading about an outdated model. In the OpenJDK canon, “primitive classes” in that sense no longer exist.
More things fell along the way. The original “Value Objects” JEP draft was withdrawn and replaced by JEP 401. The original “Universal Generics” draft also went back for rework. JEP 401 is accompanied by JEP 402: Enhanced Primitive Boxing (also preview), plus a whole series of early-access builds (LW1, LW2, LW3…) and talks from the JVM Language Summit, among them Frédéric Parain on heap flattening and Daniel Smith on the new object-initialization model.
The moral of this section is this: twelve years wasn’t twelve years of “writing code.” It was twelve years of rejecting ideas, until the one that can actually be maintained was left.
4. How Valhalla works today - the value class model in JDK 28
Let’s get to specifics. Here’s exactly what we get.
Declaration. You create a value class by adding the value modifier:
value class USDCurrency implements Comparable<USDCurrency> {
private int cents; // implicitly final
public USDCurrency(int dollars, int cents) {
this.cents = dollars * 100 + cents;
}
public USDCurrency plus(USDCurrency that) {
return new USDCurrency(0, this.cents + that.cents);
}
// dollars(), cents(), compareTo(), toString()...
}It can also be a value record. The rules: all instance fields are implicitly final, methods may not be synchronized, the class is final by default (or it can form a hierarchy composed of value classes and abstract value classes), it can’t inherit from a class with identity, but it happily implements interfaces. Beyond these constraints, it’s an ordinary class.
The defining trait: no identity. This is the crux. An ordinary object has identity: two separately created new Point(1,2) are two different objects, even if they have identical contents. A value object has no identity, just as there aren’t two “different” fours of type int. From this flow all the consequences:
== changes meaning. Until now == compared identity (whether it’s the same address). For value objects, == checks substitutability: whether both values are the same class with the same fields, compared recursively (primitive fields bit by bit, object fields again via ==). That’s why new USDCurrency(3,95) == new USDCurrency(3,95) returns true. That’s good news: it ends the famous confusion with == on Integers. But careful: == looks at internal state, which isn’t always what the object represents, so for “is this the same data” comparisons keep using equals.
synchronized throws. There’s nothing to synchronize on. An attempt ends in IdentityException. When you need to force identity, you have the new helpers Objects.requireIdentity and Objects.hasIdentity.
And now the most important conceptual trap: value objects can STILL be null. This surprises everyone who thinks “value = like a primitive = never null.” In the JDK 28 model, value class is a reference type, so USDCurrency d = null; is perfectly legal. Non-nullable types (with a null restriction) are a separate, future JEP. They’re not in JDK 28. We’ll come back to this, because it’s not a detail: it’s the key to full performance.
How it sits in memory
JEP 401 gives the JVM freedom thanks to which value objects can be optimized in two main ways.
Scalarization is a JIT compiler technique. A reference to a value object gets “broken down into its prime factors,” reduced to its essence, the set of fields, with no wrapping. Instead of passing a pointer to Color, the JIT simply passes three bytes r, g, b (plus one flag bit indicating whether the reference isn’t null). Such an object is in practice free: no allocation, no work for the GC. It’s a bit like escape analysis, but much more predictable and far-reaching: it works even across the boundaries of method calls the JIT didn’t inline. The limitation: scalarization usually won’t work when a variable has a type that is a supertype of the value class (e.g. Object or, importantly, an erased generic parameter). Then the object has to be materialized on the heap.
Heap flattening is the second mechanism. The object’s essence gets encoded as a compact bit vector and written directly into a field or an array cell, without a pointer to another place in memory. This is exactly where density and locality are born.
There’s a catch worth knowing about here, though: flattened data has to be readable and writable atomically (otherwise it risks “tearing” under concurrent access). On typical platforms, “small enough” today means as little as 64 bits, including the null flag. That’s why many small value classes will flatten beautifully, but a class with, say, two int fields or one double may not fit in an atomic write and end up as an ordinary object on the heap anyway. In the future, 128-bit encodings will arrive, and the aforementioned JEP about null-restricted types will allow flattening larger classes in exchange for giving up the atomicity guarantee. This is exactly the moment where non-nullability stops being cosmetics and becomes a lever for performance.
Boxing and unboxing in a new light
Remember the age-old cost of boxing, wrapping int in Integer? In the new model, the wrapper classes themselves become value classes (when preview is on, Integer, Long, Double, and company lose their identity). Since the box no longer has identity, the JVM can scalarize and flatten it. The effect: Integer[] starts approaching the efficiency of int[], and the boxing overhead, to quote JEP 401, shrinks dramatically. The accompanying JEP 402 (Enhanced Primitive Boxing) goes further and smooths out conversions between primitives and their boxes, opening the road to writing things like List<int>. But that’s a separate, still-maturing piece, so don’t assume it’ll roll in complete alongside 401.
Arrays
This is where the effect shows best. Instead of holding a million pointers to a million scattered objects, a Color[] array can store directly flattened, 32-bit encodings of successive colors (again: plus a null flag). From a memory standpoint, such an array starts to look and act like a plain int[]: a contiguous block of data the processor sweeps through sequentially, cache line by cache line.
For all of this to work, some really deep foundations were moved: the new value modifier; strict construction rules (all fields must be set before anything gets to see the new object, in practice before the super() call, so that a “mutation” of final fields can never be observed); the redefinition of == as a substitutability test; adding a value-object check to the reference-comparison bytecode (acmp); the scalarization and flattening machinery; IdentityException; and the migration of existing “value-based” classes. In short, this isn’t syntactic sugar. It’s a rebuild of an assumption that had been true in Java since 1995: that every object has identity.
5. A practical example - before and after, step by step
Let’s take the simplest possible case and trace it so it’s clear even without knowing the JVM’s internals.
Before Valhalla:
final class Point { // an ordinary class with identity
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];What’s happening here in memory? The points array is a million pointers. Each pointer leads to a separate Point object lying somewhere on the heap. And each such object is not just its two ints (8 bytes), but also a header (another dozen-or-so bytes of metadata). These objects are scattered: the allocator created them at different moments, in different places. When you iterate over the array and sum the coordinates, for each point the processor has to: read the pointer from the array, jump to the indicated address (risk of a cache miss), read the fields. A million times. This is exactly that “fluffy” layout from section 1.
After Valhalla:
value class Point { // a value class without identity
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 difference in the code is exactly one word: value. But the difference in memory is fundamental. The JVM can now store the values themselves in the array, laid out densely one after another: 8 bytes per point (plus a possible null flag), in a contiguous block. No headers per element. No pointers. No jumping around the heap.
[IMAGE: the same Point[] array in two variants: “before” (an array of arrows → scattered boxes with headers) and “after” (a uniform strip of number pairs)]
When you now iterate over the array, the processor reads the data sequentially. Each 64-byte cache line immediately brings in several complete points. Summing a million coordinates running at memory-bandwidth speed, instead of choking on misses. On data-intensive code that can be a difference of multiples, not percentages.
And, most important for maintainability, you didn’t pay for it with abstraction. Point is still a class: it has a name, it has a constructor, it could have validation (if (x < 0) throw ...), it could have methods. You don’t have to, like before, split points into two raw int[] xs and int[] ys arrays and pray you never mix up the indices. You got the density of a primitive and the readability of a class. That’s the whole of Project Valhalla in a single example.
6. Specialized generics - why type erasure hurts (and what’s being done about it)
This is the second half of Valhalla, and honestly the harder one. Let’s start with the source of the problem.
Java implements generics through type erasure. In practice: List<String> and List<Integer> are, at runtime, the same ordinary List, and the type parameter T is erased to Object. This often gets mocked, but it’s worth knowing it was a deliberate, defensible decision, not laziness. Erasure gave Java gradual migration compatibility: you could take an existing, non-generic class and make it generic without breaking a single existing source file or compiled class, and clients could migrate right away, later, or never. In 2004, when Java already had a huge codebase, the alternative (”here are generics, but throw out all your libraries”) would have been a terrible deal. Today it would be even worse.
The trouble is that erasure clashes with Valhalla exactly where we’d care most about performance. Since T erases to Object, a value object put into a List<Point> has to be materialized as an ordinary object on the heap. In other words: your beautiful, flattenable Point in a generic collection loses its flattening: the container holds references, not flat values. All the density you gained in Point[] evaporates in ArrayList<Point>.
The repair plan is, like all of Valhalla, two-phase:
Phase 1: Universal Generics. This is a change at the language level: it lets type variables also cover value types, that is, so you can even express something like ArrayList<Point> or List<int>. For now still through erasure. The programmer will feel it mainly as new compiler warnings about “null pollution,” because a field of type T starts out as null by default, even if T is a value type. Addressing these warnings makes APIs “specialization-ready.”
Phase 2: Specialized Generics. These are the future JVM extensions that will generate heterogeneous, specialized class layouts for concrete type arguments (in the project’s jargon: species and type restrictions). Only then will ArrayList<Point> really be backed by flat memory. This is the piece that’s still largely research work.
The consequences for libraries and frameworks are enormous, and that’s exactly why it’s happening gradually. Ultimately, collections, streams, and entire APIs can become flat and allocation-free over value types. But library authors will have to address the new warnings and design with specialization in mind. Let’s be honest: the original Universal Generics draft went through rework, and the full reward from specialization is a matter of future releases. JDK 28 doesn’t bring it.
7. What entering JDK 28 specifically means
Let’s gather this in one place, because it’s easy to get lost between “it’s here already!” and “it’s not here yet.”
What got accepted: JEP 401 (Value Classes and Objects) as a preview feature, targeting JDK 28 (release in March 2027), with integration into mainline planned for roughly July 2026. 197 thousand lines, 1,816 files, coordination on Lois Foltan’s side, the request to other committers to hold off on large changes. Disabled by default: to play with the syntax, you have to flip on --enable-preview.
What actually reaches users: the ability to declare value class and value record; migration of existing “value-based” classes in the JDK (among them the primitive wrappers like Integer) to value classes under preview; scalarization and flattening for qualifying classes; cheaper boxing.
What can still evolve, and what’s NOT in 28: null-restricted types (non-nullable); full specialized generics; 128-bit encodings; a fully mature JEP 402. And the syntax itself, because this is preview, and that’s exactly what’s expected of it: that it can change from release to release in response to feedback. Hence Goetz’s quote about “only the first part.”
How it might affect the ecosystem: for high-performance Java (data, vector computation, ML, gamedev, finance, codecs) this is the path to dense data without giving up abstraction, which is exactly what some of these domains have been waiting years for. Frameworks and libraries will start migrating their value-based classes. You’ll also have to watch out for a long tail of behavioral surprises around == and synchronized in code that (knowingly or not) relied on identity. And one more thing worth keeping in mind when planning: JDK 28 is not an “LTS” release: the next LTS will probably be JDK 29 in September 2027. So most companies will meet a stabilized Valhalla only at the LTS, but it’s precisely the preview in 28 that kicks off the real feedback loop with actual code. If you’re working on something that could benefit from this, now is the moment to start experimenting and submitting feedback.
8. Summary
Why do I call this one of the biggest changes in the platform’s history? Because Valhalla doesn’t bolt yet another feature onto the language; it moves its deepest assumption. “Every object has identity” had been true in Java since 1995; it’s the foundation everything else stood on. Letting the programmer opt out of that assumption (choose which objects need identity and which don’t) isn’t a refactor, it’s a shift of the foundation. And that’s exactly why it unlocks a whole decade of further work: unifying primitives and objects, specializing generics, denser collections, faster numerics.
At the same time, and this is the honest version of the headline, “Valhalla rolls into JDK 28“ is a half-truth. It’s the first, preview step of a multi-stage rollout. But it’s precisely this team’s discipline (simplify the model for the human, do the hard performance things as optional) that’s the reason it took twelve years, and the reason it can be shipped at all now.
For us, as programmers, one thing to take away matters more than the syntax: internalize the distinction identity versus value. The rest (==, flattening, generics) are consequences of that one distinction. And the early-access builds are already here: you can touch this on your own code before your competitor does.
And finally - the questions I keep running into 😊
1. Is value class just a record? No, they’re two orthogonal decisions. record means “I give up separate internal state” (content = components). value means “I give up identity.” You can have any combination: an ordinary class, a record, a value class, and a value record.
2. Can I compare value objects with ==? Yes, but == now means something different: substitutability, i.e. a comparison of all fields (recursively), not the address in memory. For the question “do they represent the same data,” it’s still usually better to use equals, because == looks at internal state, which isn’t always equal to the represented state.
3. Can a value class be null? In the JDK 28 model, yes. value class is still a reference type. Non-nullable types (with a null restriction) are a separate, future JEP, and they’re the ones that will unlock flattening of larger value classes. They’re not in JDK 28.
4. Integer becomes a value class, won’t that break my code? In most cases, no. Binaries still link, and the only new compilation errors are attempts to synchronize on such a type. The changes you might notice concern code that depends on identity: == on Integers will start comparing by value, and synchronized (someInteger) will stop working. If you relied on either of those, it was fragile code anyway.
5. Will I get a fast, flat ArrayList<Point>? Not yet. Because of type erasure, objects in a generic collection are materialized on the heap. Flat generic collections require universal and specialized generics: that’s the future. In JDK 28, flattening works directly for fields and arrays of a value type, e.g. for Point[].
6. How is this different from struct in C#? A struct in C# has identity and mutation, so the semantics of copying on assignment or passing have to be precisely defined, which gives a heavier model for the programmer and less freedom for the runtime. Value objects in Valhalla have no identity, and the way they’re laid out in memory is left to the JVM’s discretion. A simpler model for the human, more freedom for the machine.
7. Wasn’t escape analysis doing all of this already? As I already mentioned, partly. Escape analysis can avoid allocating an object when it proves the object doesn’t depend on identity, but it’s unpredictable and doesn’t help when the object lands in a field, in an array, or “escapes” beyond the optimization’s reach. Scalarization of value objects is predictable and reaches much further, including across method-call boundaries.
8. Do I have to rewrite code to benefit? For your own classes, it’s usually enough to add the value modifier to those that represent “simple domain values” and don’t rely on identity; the migration is mostly compatible. Some of the gains you’ll even get for free, because it’s the JDK migrating its own classes (like the primitive wrappers).
10. When will I see full Valhalla, with generics, non-null types, and the whole rest? In future releases. The team ships it incrementally: JDK 28 is the first preview of value classes. The full story (specialized generics, null-restricted types, 128-bit encodings) will spread across many releases and will most likely stabilize only around the next LTS.
PS: You’ll find the early-access builds at jdk.java.net/valhalla, and that’s probably the best way to form your own opinion faster than I can write another issue on the subject.











