Skip to content

Java 27 Shrinks Object Headers, JDK 28 Previews Value Objects: Upgrade Now, Audit for Identity Next

•
13 min read

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.

Mermaid diagram

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:

ReleaseJEPStatusHow to enable
JDK 24JEP 450Experimental-XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders
JDK 25JEP 519Product feature, off by default-XX:+UseCompactObjectHeaders
JDK 27JEP 534DefaultNothing. 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:-UseCompactObjectHeaders as 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:

Terminal window
# Baseline: legacy 96-bit headers
java -XX:-UseCompactObjectHeaders -Xlog:gc*:file=gc-legacy.log -jar service.jar
# Candidate: the JDK 27 default
java -Xlog:gc*:file=gc-compact.log -jar service.jar

What to compare:

MetricWhy it mattersHow to get it
Live set after full GCThe most direct measure of header savingsGC log, or jcmd <pid> GC.class_histogram
GC count and pause timesA smaller heap means fewer collections at the same allocation rateGC log
Container RSSThis is what your orchestrator bills and kills onContainer metrics
Throughput and p99 latencyConfirms nothing regressedYour 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 value modifier makes the class implicitly final, and all its fields implicitly final. Records can be value record.
  • == on value objects compares field values. The JEP says redefining == as a replacement for equals is a non-goal, and you should keep using equals.
  • 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, ZonedDateTime and Duration. String is 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: true

Using it requires the preview flag at both compile and run time:

Terminal window
javac --release 28 --enable-preview -d out $(find src -name '*.java')
java --enable-preview -cp out com.example.Main

Once 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 operationBehaviour with a value objectSource
synchronized (v) where the static type is a value classCompile errorJEP 401
synchronized (o) where o is an Object that refers to a value objectIdentityException at run timeJEP 401
v.wait() / v.notify()Always IllegalMonitorStateExceptionJEP 401
new WeakReference<>(v), any java.lang.ref.ReferenceIdentityExceptionJEP 401
Value object as a WeakHashMap keyCannot be used with value objectsJEP 401
a == bCompares field valuesJEP 401
System.identityHashCode(v)Computed from field values, same as the inherited hashCodeJEP 401
Overriding finalize()Never called. javac emits an identity warningJEP 401
IdentityHashMap keyDistinct-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 time
private 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/hashCode change meaning. JEP 401 advises overriding equals and hashCode explicitly before you migrate a class. That way the behaviour you rely on is written down, not inherited from Object.
  • Sensitive data. Because == and System.identityHashCode now 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.

Terminal window
# 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 see
java -XX:DiagnoseSyncOnValueBasedClasses=2 ... # log to console and JFR
java -XX:DiagnoseSyncOnValueBasedClasses=1 ... # treat as a fatal error (use in CI)
  • -Xlint:synchronization (JEP 390) flags synchronized on value-based class types. Don’t let a -Xlint:-synchronization hide in your build config.
  • -XX:DiagnoseSyncOnValueBasedClasses (JEP 390) is the important one, because it catches the Object-typed case from the example above. Run your integration tests with =1 so any hit fails the build, and run a canary with =2 to collect events in JFR.
  • javac identity warnings have fired since JDK 25 when value-based classes are used with java.lang.ref or WeakHashMap (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:

ScalarizationHeap flattening
WhereJIT-compiled code (locals and registers)Fields and array elements
Size limitNot constrained by data sizeLimited by atomicity
Typical budgetn/aAs small as 64 bits including a null flag (InfoQ)
During warmup / interpreterNot applied, ordinary allocationMay be used but can cost more to access

In practice:

  • Single-int wrappers are the ideal case. A Quantity wrapping one int needs 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. An OrderId wrapping a long is 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 consume Money values.
  • 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:

  1. 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).
  2. A/B test -XX:-UseCompactObjectHeaders against the default. Measure live set after full GC, GC count, pause times, container RSS and p99.
  3. Pin your GC explicitly so the G1-everywhere change (JEP 523) doesn’t muddy the comparison.
  4. Only keep the opt-out if you measured a regression. The legacy layout is headed for deprecation.

Before JDK 28:

  1. Run CI with -XX:DiagnoseSyncOnValueBasedClasses=1 and fix every hit by locking on a dedicated object.
  2. Make -Xlint:synchronization and javac identity warnings fatal.
  3. Grep for IdentityHashMap, WeakHashMap, WeakReference, System.identityHashCode and == on boxed types or java.time values.
  4. Override equals/hashCode on your candidate value types now.
  5. Shortlist small immutable types (IDs, quantities, money) as value record candidates. Don’t expect big DTOs to flatten.
  6. 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

  1. JEP 534: Compact Object Headers by Default
  2. JEP 519: Compact Object Headers
  3. JEP 450: Compact Object Headers (Experimental)
  4. JEP 401: Value Objects (Preview)
  5. JEP 390: Warnings for Value-Based Classes
  6. The Arrival of Java 27! (Inside.java)
  7. Java 27 Delivers Post-Quantum Cryptography, Future Language Innovation, Helidon 27, JavaFX 27 (InfoQ)
  8. Project Valhalla’s First Preview: JEP 401 Redefines == for Java Objects (InfoQ)
  9. JEP proposed to target JDK 28: 401: Value Objects (Preview) (jdk-dev)
  10. JEP draft: Null-Restricted Value Class Types (Preview)
  11. Value Classes Heap Flattening - What to expect from JEP 401 #JVMLS (Inside.java)
  12. Value Objects (Project Valhalla)

Written by

Farshad Akbari

Software engineer writing about Java, Kotlin TypeScript, Python, data systems and AI

Keyboard Shortcuts

Navigation

  • Open search ⌘K
  • Next article j
  • Previous article k

Actions

  • Toggle dark mode d
  • Toggle table of contents t
  • Show this help ?
  • Close modal Esc

Shortcuts are disabled when typing in inputs or textareas.