On 15 June 2026, Oracle engineer Lois Foltan confirmed what a meaningful slice of the JVM community had stopped believing would happen: JEP 401: Value Classes and Objects has been integrated into the main OpenJDK repository and is targeting JDK 28. The pull request adds more than 197,000 lines of code across 1,816 files. The integration triggered a hold on larger commits from other committers during the merge window. Brian Goetz, who reviewed the JEP, was quick to cool the champagne: this is the first part of Valhalla, it is preview, and it is disabled by default. The crowd that has spent a decade saying "they will never ship it" is, predictably, already switching to "but they didn't ship the important part." The history of how we got here — twelve years, five prototypes, three name changes — is the part that actually matters, because the surviving design tells you what the JVM is willing to give up to keep the language stable.
The problem the project was created to solve
Java has eight primitive types — int, long, double, boolean, and friends — and everything else is a reference type. When you write Point p = new Point(1, 2), p is not a point. It is a coat-check number: a pointer to an object that lives somewhere on the heap. Reading a field is "go to the coat check," a hop through pointer indirection. For a single object that is nothing. The cost starts at scale.
Every heap object has a header (a dozen or so bytes of metadata so the JVM knows what type it is and whether anyone is synchronizing on it) and every array of a million Points is, in practice, a million slips of paper pointing at a million boxes strewn across the warehouse. Brian Goetz calls such a layout "fluffy" — puffed up, bloated. The opposite is a "dense" layout where data lies side by side. The reason density matters is that 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 entire gap is bridged by the cache. The processor reads memory in 64-byte cache lines. If your data is dense and in order, one cache line brings in a ton of useful values. If your code is 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 is the actual stake in the entire Valhalla effort.
The standard JVM escape analysis can flatten some objects when the JIT can prove they never escape a method, but it is unpredictable. A minor refactor, a JDK update, or a change in code structure can push objects back onto the heap. Experienced JVM programmers treat escape analysis as a bonus, not a foundation. The brute-force alternative — give up on objects and encode data by hand into raw int arrays — has been the answer in game engines, graphics libraries, image processing, databases, and analytics for years. The cost is safety and readability. Valhalla is the attempt to erase the dichotomy.
The five prototypes that died on the way to L World
Officially, Project Valhalla started in 2014. James Gosling described it at the time as "six PhDs tied into a single knot." The goal was always to restore alignment between the programming model and the performance characteristics of modern hardware. The path was not. Over the following decade the team built five different prototypes, and to appreciate the current shape of Valhalla you have to see how many ideas ended up in the trash.
The earliest prototypes went in a direction that is now called "Q World." Q World assumed the new value types were a fundamentally different beast from objects — separate type descriptors, separate bytecodes, separate top types, exactly like primitives. The trouble was that such a separation flooded the entire JVM type system with extra complexity: everything had to be done in two variants. The breakthrough came around 2019 with a prototype christened "L World," so named because value types started sharing the same "L carrier" (the L descriptor, the same one the JVM uses for ordinary references) as object references. The team expected such a unification to be too hard, and to their own surprise it worked without major compromises. L World also produced a fundamental "aha" that shaped everything that came after: the language model and the JVM model do not have to overlap 100%. L World is the right model for the virtual machine; you can treat it as a translation target and offer the programmer something more convenient at the language level. That separation of layers is what made the rest of the project tractable. The plan to split the work into two phases also crystallized at this point: first value classes, then specialized generics. Generics is the separate, harder treatise that we will return to.
The naming rollercoaster is a history of rejected ideas
If you have ever tried to read about Valhalla and bounced off a wall of contradictory terms, the problem is not you — the naming changed several times, and each name change tracked a change in the underlying model.
Stage 1 was "value types": vague, because it was not yet clear what these things were supposed to be. Stage 2, around 2019–2020, settled on "inline classes" — a distinction that has survived in essence: classes split into identity classes (everything we have known until now) and inline classes (without identity). The slogan "codes like a class, works like an int" was coined then. Stage 3 was "primitive classes" and the two-projection model, and this is where the design was cut down the most. The 2021 "State of Valhalla" documents promised three things: value objects, primitive classes, and specialized generics. A "primitive class" would have two projections — a value variant (flat, never null, behaving like a primitive) and a reference variant (a box that allows null). Across iterations this was written as Point.val / Point.ref, and the team later experimented with Point! and Point? syntax. The model was powerful but mentally heavy. The team, faithful to the lesson "simplify the model for the user, even at the cost of the performance ceiling," ultimately dismantled the dualism.
Stage 4 — today — is "value classes" and "value objects." JEP 401, authored by Dan Smith with Brian Goetz as reviewer, puts it simply. There is one new thing: a value class, declared with the value modifier. Its instances are value objects: objects without identity. 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) that is not in JDK 28. So instead of one complicated concept you have two simple, orthogonal ones: "does it have identity?" and, separately, for later, "does it allow null?" Twelve years was not twelve years of "writing code." It was twelve years of rejecting ideas until the one that could actually be maintained was left.
What you actually get in JDK 28
The change at the source level is exactly one word. A value class is declared 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);
}
}
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 cannot inherit from a class with identity, and it happily implements interfaces. Beyond these constraints it is an ordinary class.
The defining trait is no identity. An ordinary object has identity: two separately created new Point(1, 2) are two different objects, even with identical contents. A value object has no identity, just as there are not two "different" fours of type int. From this flow all the consequences. == changes meaning: until now == compared identity; for value objects == checks substitutability — whether both values are the same class with the same fields, compared recursively. That is why new USDCurrency(3, 95) == new USDCurrency(3, 95) returns true. It also ends the famous confusion with == on Integer. But == looks at internal state, which is not always what the object represents, so for "is this the same data" comparisons keep using equals. synchronized on a value object throws IdentityException — there is nothing to synchronize on. When you need to force identity, you have the new helpers Objects.requireIdentity and Objects.hasIdentity.
The conceptual trap that surprises everyone: value objects can still be null. In the JDK 28 model, value class is a reference type, so USDCurrency d = null; is perfectly legal. Non-nullable types are a separate, future JEP. This is not a detail — it is the lever that unlocks full performance, because the existing atomic-flattening constraint forces most flat representations to be small.
How it sits in memory: scalarization and heap flattening
JEP 401 gives the JVM two main optimizations. Scalarization is a JIT compiler technique: a reference to a value object is "broken down into its prime factors" — 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 for null. Such an object is in practice free: no allocation, no work for the GC. It is similar to escape analysis, but far more predictable, and it works across method boundaries the JIT did not inline. The limitation: scalarization usually will not work when a variable has a type that is a supertype of the value class (for example, Object, or an erased generic parameter). Then the object has to be materialized on the heap.
Heap flattening is the second mechanism. The object's essence is 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 where density and locality are born. The catch is that 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. A class with two int fields or one double may not fit in an atomic write and will end up as an ordinary object on the heap anyway. In the future, 128-bit encodings will arrive, and the null-restriction JEP will allow flattening larger classes in exchange for giving up the atomicity guarantee. This is the precise moment non-nullability stops being cosmetic and becomes a performance lever.
The migration of the wrapper classes is the visible payoff. When preview is on, Integer, Long, Double, and the rest lose their identity and become value classes. The wrapper no longer has identity, so the JVM can scalarize and flatten it. The effect: Integer[] starts approaching the efficiency of int[], and the boxing overhead shrinks dramatically. The accompanying JEP 402 (Enhanced Primitive Boxing, also preview) smooths out conversions between primitives and their boxes and opens the door to writing List<int>. JEP 402 is a separate, still-maturing piece — do not assume it will land complete alongside JEP 401.
A practical example: before and after, step by step
Take the simplest possible case. Before Valhalla:
final 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 array is a million pointers. Each pointer leads to a separate Point object somewhere on the heap. Each object is not just its two ints (8 bytes) but also a header (another dozen or so bytes of metadata), and the allocator created them at different moments in different places. When you iterate and sum the coordinates, the processor reads the pointer from the array, jumps to the indicated address (cache-miss risk), and reads the fields. A million times. After Valhalla:
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 difference in source is exactly one word. 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), contiguous. No headers per element, no pointers, no jumping around the heap. Each 64-byte cache line immediately brings in several complete points. Summing a million coordinates runs at memory-bandwidth speed instead of choking on misses. On data-intensive code the gain is multiples, not percentages. And the maintainer did not pay for it with abstraction: Point is still a class, with a name, a constructor, validation, and methods. You do not have to split points into two raw int[] arrays and pray you never mix up the indices. That is the whole of Project Valhalla in a single example.
The original take: specialized generics is the part that matters, and it is not in this build
The headline reaction to the JDK 28 announcement has been "value classes are here, finally." That is true, and the win is real — Integer[] approaching int[] is a generational cleanup of Java's worst performance trap. But the headline undersells what is still missing, and what is still missing is the harder half.
Java implements generics through type erasure. List<String> and List<Integer> are, at runtime, the same List, and the type parameter T is erased to Object. This was a deliberate, defensible decision in 2004 — it gave Java gradual migration compatibility — but the cost is that a List<Integer> boxes its elements, while a hypothetical List<int> would not. Valhalla's specialized generics is the half that fixes this. Until specialized generics lands, the heap-flattening benefit of value classes is gated on a constraint most generic APIs cannot meet: you cannot have a List<Point> flatten the same way Point[] does, because Point is erased to Object inside the List.
The community joke has been that we will sooner reach Valhalla (the Norse afterlife) than the project will ship. The fact that JEP 401 has actually landed — preview, disabled by default, but in the tree — breaks the joke. The follow-up joke is "they shipped the easy half." That one is also probably true. Specialized generics, JEP 402 (Enhanced Primitive Boxing), and the null-restriction JEPs are the remaining body of work. None of them have a target JDK yet. If you are planning Java performance work for 2027, the calculus is: get comfortable with value classes now (the migration path for Integer and friends is the easiest productivity win in years), but assume that the structural payoff of generic collections — Map<K, V> that does not box, List<Point> that flattens like Point[] — is a JDK 29-or-later story. Plan around the constraint, not the promise.
What this means for you
If you maintain a Java library, the move for the next six months is to identify your public types whose instances are conceptually immutable data — Money, Color, Coordinate, DateRange, EmailAddress, the obvious suspects — and check whether they are eligible for a value class conversion. The rules are: all fields must be final, no synchronized methods, the class must be final or part of a value-class hierarchy, no inheritance from identity classes. Most DTOs and value objects already satisfy those constraints. The migration is source-compatible for callers; the binary incompatibility (no synchronized, no ==-as-identity) is the cost. Migrate your internal data classes first. Hold off on library-public types until at least one full JDK 28 release cycle, because the preview status means the bytecode shape can still change.
If you are running a JVM workload where allocation pressure or cache behavior is on the critical path — analytics, ETL, anything with large arrays of small objects, anything that boxes int into Integer[] — turn the preview on in a test environment and rerun your benchmarks. The expected gains are not "10% faster." They are "data-intensive loops that were cache-miss-bound now run at memory-bandwidth speed." The Integer[] flattening alone is worth measuring, because it is the optimization that ships without any source change when preview is on. Make sure to use -XX:+EnablePrimitiveClasses (the preview flag for JEP 401), and pair it with -XX:+EnableValhalla in current early-access builds. Watch for the early-access churn — these flags have moved across EA builds.
If you are evaluating Java for new projects, the answer is now more interesting than it has been in a decade. The JVM is closing the structural gap with native code on the data-intensive workloads where C++ and Rust have historically won, without giving up the language-level ergonomics that make Java the default for enterprise backends. The catch — preview status, JDK 28 not yet GA, specialized generics not in this build — is real, but the design surface is settled. The remaining work is engineering, not design.
What to do this week
STEP 1. Read JEP 401 end to end. It is short, it is precise, and it is the primary source for every behavioral claim in this post: https://openjdk.org/jeps/401. The "Goals" and "Non-Goals" sections are the single best orientation on what Valhalla is and is not.
STEP 2. Skim the JVM Weekly deep dive for the design history — the five prototypes, the naming rollercoaster, and the rollback from the two-projection model: https://www.jvm-weekly.com/p/project-valhalla-explained-how-a. It is the only public source that traces the rejected ideas in order.
STEP 3. Clone the OpenJDK Valhalla early-access build and turn the preview on. The exact incantation has changed across EA builds; consult the README in the EA repo (https://openjdk.org/projects/valhalla/). Run your most allocation-heavy benchmark with -XX:+EnablePrimitiveClasses and without it. Record the difference, especially for Integer[]-shaped workloads.
STEP 4. Audit your public API surface for candidate value classes. For each candidate, check: are all fields final? Any synchronized? Does it inherit from a non-value class? Does anything call synchronized on an instance? The four-question checklist catches 90% of eligibility decisions.
STEP 5. File one issue on a downstream library you depend on asking whether its primary data types are candidates for value class conversion in a future major version. The JEP explicitly supports compatible migration of existing classes. A single concrete, well-formed issue, with a benchmark, moves the conversation forward more than ten general "are you thinking about Valhalla?" posts.
# Concrete, copy-pasteable audit. Run from your project's root.
# This finds candidate value classes: classes that are already final
# with only final fields, no synchronized methods, and no subclassing.
find src/main/java -name '*.java' -print0 \
| xargs -0 grep -l 'final class' \
| while read f; do
if ! grep -q 'synchronized' "$f" \
&& ! grep -qE 'extends [A-Z]' "$f" \
&& ! grep -qE 'class [A-Z][A-Za-z0-9_]* *extends' "$f"; then
echo "CANDIDATE: $f"
fi
done
# Compare a benchmark against JDK 28 EA with the preview disabled vs enabled:
java -XX:-EnablePrimitiveClasses -jar target/benchmarks.jar -wi 3 -i 5
java -XX:+EnablePrimitiveClasses -jar target/benchmarks.jar -wi 3 -i 5
# What you should see in your audit output (illustrative, your repo
# will differ — this is a sample from a Spring-Boot-style service):
# CANDIDATE: src/main/java/com/example/money/Money.java
# CANDIDATE: src/main/java/com/example/geo/Coordinate.java
# CANDIDATE: src/main/java/com/example/range/DateRange.java
# CANDIDATE: src/main/java/com/example/contact/EmailAddress.java
The 2026 bet on Java got more interesting this week, and not because Java changed its mind. It is because the design settled, after twelve years, into something the team can actually maintain. The full payoff is still a few JDK releases out. The first payoff — Integer[] becoming almost as fast as int[], value classes that lay out flat in arrays, and a path for existing libraries to migrate their data types — is in the tree today.
Disclosure
This post was drafted with AI assistance. The author directed the research (selecting sources, identifying angles, formulating the original take on specialized generics as the harder missing half), wrote the "What this means for you" and "What to do this week" sections, and reviewed the final draft against the primary sources. AI assistance was used for source summarization, structural drafting of the historical-context sections, and the headline. Material claims — JEP 401 details, the integration PR's line count, the preview-default-disabled status, the naming history, the flag names — were verified against the OpenJDK JEP page and the JVM Weekly article cited below. Errors remaining are the author's. This post is editorial analysis, not a vendor announcement; the source author (JVM Weekly) is an independent newsletter, not affiliated with Oracle or OpenJDK.
Sources
- Project Valhalla, Explained: How a Decade of Work Arrives in JDK 28 — JVM Weekly vol. 180 (Artur Skowronski, 18 June 2026) — primary source for the historical narrative, the five-prototype arc, the naming rollercoaster, and the integration PR line count (197,000+ lines across 1,816 files).
- JEP 401: Value Classes and Objects (Preview) — OpenJDK (Dan Smith, reviewer Brian Goetz; updated 17 June 2026) — primary source for all behavioral claims: the
valuemodifier, the==redefiniton, theIdentityException, the goals/non-goals framing, and the explicit preview/default-disabled status. - OpenJDK: Project Valhalla — landing page for early-access builds, current flag names, and the broader JEP roadmap (JEP 402 Enhanced Primitive Boxing, the deferred null-restriction JEPs, specialized generics).
- Brian Goetz — State of Valhalla (2021) — historical design document for the two-projection model that was later dismantled; useful for understanding what Valhalla rejected, not just what it kept.
No comments:
Post a Comment