Java 27 Shrinks Object Headers, JDK 28 Previews Value Objects: Upgrade Now, Audit for Identity Next
Two Releases, One Memory Story
JDK 27 went GA on September 15, 2026. The change most likely to show up on your dashboards is not a language feature. It’s the object header: JEP 534 makes compact object headers the default in HotSpot, so every object header on your heap shrinks from 96 to 64 bits, and you don’t have to change any code.
The next step is already queued up. JEP 401: Value Objects (Preview) was proposed to target JDK 28 on 24 July 2026. The JEP page now lists it as Integrated for Release 28, and InfoQ reports that it’s in the JDK mainline alongside JEP 539. JDK 28 is expected in March 2027. Value objects give up object identity, which lets the JVM flatten and scalarize them. They also break any code that quietly depends on identity.
Both releases change how your objects sit in memory. JDK 27 does it for you. JDK 28 needs your codebase to be ready first. The plan: upgrade to 27 now, and prepare the codebase for 28.
What Changed in the Object Header, and What to Benchmark
On 64-bit HotSpot, every object used to carry a 96-bit header. Compact headers bring that down to 64 bits. The feature took three JEPs to become the default:
| Release | JEP | Status | How to enable |
|---|---|---|---|
| JDK 24 | JEP 450 | Experimental | -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders |
| JDK 25 | JEP 519 | Product feature, off by default | -XX:+UseCompactObjectHeaders |
| JDK 27 | JEP 534 | Default | Nothing. Opt out with -XX:-UseCompactObjectHeaders |
Some details from JEP 534 that matter for production:
- The old 96-bit layout is still there. Removing it is not a goal of this JEP, but the JEP says it is intended to be deprecated in a future release. Treat
-XX:-UseCompactObjectHeadersas an escape hatch, not a long-term setting. - The feature has real production mileage. Amazon runs hundreds of services with compact headers, mostly on backports to JDK 21 and 17, and SAP’s SapMachine already enables them by default.
- Four header bits are already reserved for Project Valhalla. The header work and the value-object work are part of the same plan.
Where “10–20%” comes from
You’ll hear “compact headers save 10–20% of heap”. That number has a specific source, so be clear about what it does and doesn’t promise.
- The 10–20% range comes from JEP 450. Early adopters of Project Lilliput who tried it on real-world applications reported live data typically dropping by 10–20%. The reasoning holds up: Lilliput experiments found average object sizes of 32–64 bytes, so a 12-byte header can easily be more than 20% of live data.
- The measured benchmarks come from JEP 534. In one setting, SPECjbb2015 used 22% less heap and 8% less CPU time. In another, it did 15% fewer garbage collections with both G1 and Parallel. A highly parallel JSON parser ran in 10% less time.
These are measured results on particular workloads, not guarantees. How much you save depends on your average object size. A service that holds millions of small objects (boxed values, map entries, small DTOs) will gain more than one whose heap is mostly large byte[] buffers.
A benchmark plan for JDK 27
Run the same build on JDK 27 with the flag on and off, under the same load:
# Baseline: legacy 96-bit headersjava -XX:-UseCompactObjectHeaders -Xlog:gc*:file=gc-legacy.log -jar service.jar
# Candidate: the JDK 27 defaultjava -Xlog:gc*:file=gc-compact.log -jar service.jarWhat to compare:
| Metric | Why it matters | How to get it |
|---|---|---|
| Live set after full GC | The most direct measure of header savings | GC log, or jcmd <pid> GC.class_histogram |
| GC count and pause times | A smaller heap means fewer collections at the same allocation rate | GC log |
| Container RSS | This is what your orchestrator bills and kills on | Container metrics |
| Throughput and p99 latency | Confirms nothing regressed | Your load test |
Isolate one more variable before you blame (or credit) the headers. JDK 27 also ships JEP 523, which makes G1 the default collector in every environment. If you don’t pick a GC, HotSpot now always selects G1, including in small containers where it used to fall back to Serial. If your small pods never set -XX:+UseSerialGC explicitly, their GC behaviour changes with this upgrade too. Pin the collector in both arms of the A/B test so you measure one change at a time.
JEP 401 in One Page
A value object is immutable, has no object identity, and is distinguished only by its field values. JEP 401 adds them as a preview language and VM feature:
value record Money(long cents, Currency currency) { }
value class Quantity { private final int units; // fields are implicitly final anyway Quantity(int units) { this.units = units; }}The rules that matter in practice:
- The
valuemodifier makes the class implicitlyfinal, and all its fields implicitlyfinal. Records can bevalue record. ==on value objects compares field values. The JEP says redefining==as a replacement forequalsis a non-goal, and you should keep usingequals.- Value classes are still reference types, so a variable of a value class type can still be
null. Objects.hasIdentity(obj)tells you whether an object is an identity object.- With preview enabled, 30 platform classes become value classes, including
Integer,Long,Double,Boolean,Character,Optional,OptionalInt,LocalDate,LocalTime,LocalDateTime,ZonedDateTimeandDuration.Stringis not one of them. It stays an identity class.
That last point is where existing code starts to change behaviour. The JEP gives the example directly:
Integer x = 1996, y = 1996;System.out.println(x == y);// JDK 27, or JDK 28 without --enable-preview: false (outside the Integer cache)// JDK 28 with --enable-preview: trueUsing it requires the preview flag at both compile and run time:
javac --release 28 --enable-preview -d out $(find src -name '*.java')java --enable-preview -cp out com.example.MainOnce you compile with preview, javac uses the value-object versions of classes like LocalDate, and the resulting class files must also run with preview enabled. The InfoQ write-up also recommends recompiling migrated classes rather than mixing old and new class files, because of the new LoadableDescriptors class-file attribute.
What Breaks: An Identity Audit
Anything that asks “is this the same object?” rather than “is this an equal object?” needs a look. Here is what JEP 401 specifies for value objects:
| Identity-dependent operation | Behaviour with a value object | Source |
|---|---|---|
synchronized (v) where the static type is a value class | Compile error | JEP 401 |
synchronized (o) where o is an Object that refers to a value object | IdentityException at run time | JEP 401 |
v.wait() / v.notify() | Always IllegalMonitorStateException | JEP 401 |
new WeakReference<>(v), any java.lang.ref.Reference | IdentityException | JEP 401 |
Value object as a WeakHashMap key | Cannot be used with value objects | JEP 401 |
a == b | Compares field values | JEP 401 |
System.identityHashCode(v) | Computed from field values, same as the inherited hashCode | JEP 401 |
Overriding finalize() | Never called. javac emits an identity warning | JEP 401 |
IdentityHashMap key | Distinct-but-equal objects collapse to one key (inference, see below) | Derived from the two rows above |
IdentityHashMap is an inference, not JEP text. None of the sources name IdentityHashMap. But it’s built on exactly two things: == for key comparison and System.identityHashCode for bucketing. JEP 401 changes both to be based on field values. So two value objects with equal fields will be treated as the same key. If you use IdentityHashMap for visited-sets in graph traversal, serialization caches or proxy registries, and value objects (including boxed Integers or LocalDates under preview) can end up as keys, expect them to be deduplicated.
The synchronization failure is the one most likely to hit production, because it can hide behind Object:
// Compiles on JDK 28 with preview, fails at run timeprivate final Map<Object, Object> locks = new ConcurrentHashMap<>();
void process(Object key) { synchronized (locks.computeIfAbsent(key, k -> k)) { // if key is an Integer or LocalDate... // ...this throws IdentityException under --enable-preview }}JEP 390 already suggested the fix years ago: when you need identity, lock on a dedicated object such as new Object() or use an AtomicReference.
void process(Object key) { Object lock = locks.computeIfAbsent(key, k -> new Object()); synchronized (lock) { // identity lives in the lock object, not the key }}Two quieter hazards:
- Inherited
equals/hashCodechange meaning. JEP 401 advises overridingequalsandhashCodeexplicitly before you migrate a class. That way the behaviour you rely on is written down, not inherited fromObject. - Sensitive data. Because
==andSystem.identityHashCodenow depend on field contents, the JEP warns they could be used to infer those contents. Think twice before making a class that holds secrets a value class.
Migration aids you can turn on today
You don’t need a JDK 28 build to start the audit. The JDK has been warning about exactly this since JDK 16.
# Compile time: flag synchronized on value-based class types (on by default since JDK 16)javac -Xlint:synchronization ...
# Run time: catches sync reached through Object references, which javac cannot seejava -XX:DiagnoseSyncOnValueBasedClasses=2 ... # log to console and JFRjava -XX:DiagnoseSyncOnValueBasedClasses=1 ... # treat as a fatal error (use in CI)-Xlint:synchronization(JEP 390) flagssynchronizedon value-based class types. Don’t let a-Xlint:-synchronizationhide in your build config.-XX:DiagnoseSyncOnValueBasedClasses(JEP 390) is the important one, because it catches theObject-typed case from the example above. Run your integration tests with=1so any hit fails the build, and run a canary with=2to collect events in JFR.- javac
identitywarnings have fired since JDK 25 when value-based classes are used withjava.lang.reforWeakHashMap(JEP 401). Treat them as errors.
The classes these tools know about are the @jdk.internal.ValueBased ones listed in JEP 390: primitive wrappers, Optional*, many java.time types, Runtime.Version, ProcessHandle, and the implementations behind List.of/Set.of/Map.of. That list overlaps heavily with the classes that become value classes under preview. A clean run covers the synchronization and weak-reference rows of the table above. It says nothing about ==, identityHashCode or IdentityHashMap on boxed or java.time values. Those still need the grep in the checklist. Your own future value classes will need the same audit once you mark them value.
Modelling DTOs, Money Types and Records for 28
Not every record should become a value record, and the benefit you get depends on which optimization applies. JEP 401 describes two:
- Reference flattening stores a value object’s fields directly in the containing field or array element, with no pointer and no separate header.
- Reference scalarization keeps a value object’s fields in locals and registers inside JIT-compiled code, so no allocation happens at all.
They have very different limits:
| Scalarization | Heap flattening | |
|---|---|---|
| Where | JIT-compiled code (locals and registers) | Fields and array elements |
| Size limit | Not constrained by data size | Limited by atomicity |
| Typical budget | n/a | As small as 64 bits including a null flag (InfoQ) |
| During warmup / interpreter | Not applied, ordinary allocation | May be used but can cost more to access |
In practice:
- Single-
intwrappers are the ideal case. AQuantitywrapping oneintneeds 32 bits plus a null flag, which fits in 64. It is the most likely candidate for heap flattening, though JEP 401 doesn’t guarantee any layout. - A
long-wrapping ID is already over budget. AnOrderIdwrapping alongis at 64 bits before the null flag, so on typical platforms it probably won’t heap-flatten under JEP 401. It can still benefit from scalarization, and null-restricted types would change that. Money(long cents, Currency currency)probably won’t flatten on the heap. The null-restricted types draft notes that most value classes with two or more fields can’t be heap-flattened under atomic-size limits. Scalarization can still remove allocation in C2-compiled hot loops that create and consumeMoneyvalues.- Big DTOs won’t flatten. A 12-field request DTO is still a pointer to a heap object. Make it a value class if you want the semantics (immutability, no identity), not because you expect a layout win.
- Records change a little even if you don’t touch them. With preview enabled in Java 28, all records, value or identity, follow the same strict construction rules: fields must be set before they can be observed.
JEP 401 does not guarantee any particular memory layout. Null-restricted types (Range!), which would drop the null flag and allow flat storage no larger than the fields, are in a separate draft JEP. So are larger flattening and specialized generics. None of that is part of JEP 401 or targeted to JDK 28.
Caveats Before You Plan Around 28
It’s a preview. You need --enable-preview at compile and run time, class files compiled with preview won’t run without it, and the semantics can still change before the feature is final. Don’t enable it in production services. Use it in a branch and in CI to find out what breaks.
Flat isn’t always faster. At JVMLS 2025, Frederic Parain explained that accessing a flat value can cost more than accessing a non-flat one, especially when nullability and atomicity have to be guaranteed. The interpreter, the runtime and C1 handle flat values less well than C2, which can cause performance regressions and affect startup. The ideal case is flat arrays accessed by C2-compiled code. If you care about startup (CLI tools, serverless, fast-scaling pods), measure it.
== isn’t free anymore. The if_acmpeq bytecode gains an extra check for value objects. The identity case is meant to stay fast, but comparing large trees of value objects with == can take unbounded time.
Be skeptical of early performance anecdotes. You may see claims about specific regressions, for example around System.arraycopy on null-restricted flat arrays. Searches of the valhalla-dev list turned up no primary source for that, so treat it as unverified. Also, null-restricted arrays aren’t even part of JEP 401. Run your own benchmarks against the early-access builds the Valhalla project publishes before you believe anyone’s numbers, mine included.
Takeaways: A Checklist
Now, on JDK 27:
- Upgrade, knowing what you’re signing up for. JDK 27 is a non-LTS release (InfoQ), and Oracle provides updates only until March 2027, when JDK 28 replaces it. Moving to 27 means moving again to 28 next March. If you stay on LTS releases, run the same benchmark on JDK 25 with
-XX:+UseCompactObjectHeaders(JEP 519). - A/B test
-XX:-UseCompactObjectHeadersagainst the default. Measure live set after full GC, GC count, pause times, container RSS and p99. - Pin your GC explicitly so the G1-everywhere change (JEP 523) doesn’t muddy the comparison.
- Only keep the opt-out if you measured a regression. The legacy layout is headed for deprecation.
Before JDK 28:
- Run CI with
-XX:DiagnoseSyncOnValueBasedClasses=1and fix every hit by locking on a dedicated object. - Make
-Xlint:synchronizationand javacidentitywarnings fatal. - Grep for
IdentityHashMap,WeakHashMap,WeakReference,System.identityHashCodeand==on boxed types orjava.timevalues. - Override
equals/hashCodeon your candidate value types now. - Shortlist small immutable types (IDs, quantities, money) as
value recordcandidates. Don’t expect big DTOs to flatten. - When JDK 28 early-access builds fit your toolchain, build a branch with
--enable-preview, run the full test suite and measure startup separately from steady state.
References
- JEP 534: Compact Object Headers by Default
- JEP 519: Compact Object Headers
- JEP 450: Compact Object Headers (Experimental)
- JEP 401: Value Objects (Preview)
- JEP 390: Warnings for Value-Based Classes
- The Arrival of Java 27! (Inside.java)
- Java 27 Delivers Post-Quantum Cryptography, Future Language Innovation, Helidon 27, JavaFX 27 (InfoQ)
- Project Valhalla’s First Preview: JEP 401 Redefines == for Java Objects (InfoQ)
- JEP proposed to target JDK 28: 401: Value Objects (Preview) (jdk-dev)
- JEP draft: Null-Restricted Value Class Types (Preview)
- Value Classes Heap Flattening - What to expect from JEP 401 #JVMLS (Inside.java)
- Value Objects (Project Valhalla)