briefki
All articles

Java 21 to 27: What Actually Shipped

A digital 3D rendering showcasing a minimalist geometric structure made of white cubes arranged in a step-like formation.
Photo by Steve A Johnson on Pexels

Java has shipped a release every six months since Java 21 landed as an LTS in September 2023: 22, 23, 24, and the next LTS, 25, in September 2025, then 26 in March 2026. JDK 27 is in Release Candidate now, feature-frozen, due September 15, 2026. That’s seven releases and about a hundred JEPs in total. Most of them are internal or incubator-only. A handful actually change what you write day to day — and a few flagship features that looked “almost done” in 21 are, six releases later, still in preview.

Virtual threads: finalized in 21, actually safe to lean on in 24

JEP 444 finalized virtual threads in Java 21 — the headline feature of the whole cycle. But finalized didn’t mean production-ready for every codebase. Any code that entered a synchronized block or method on a virtual thread pinned that thread to its underlying platform thread for the duration, silently capping concurrency for exactly the kind of legacy code (older JDBC drivers, synchronized-heavy libraries) most likely to be running under a thread-per-request migration. JEP 491, finalized in JDK 24, removed that restriction: a virtual thread blocking inside synchronized now releases its carrier instead of pinning it. If a virtual-threads migration stalled in 2024 because profiling showed threads pinning under load, that’s the JEP that fixes it — no code changes required, just the JDK upgrade.

Simpler entry points, finalized in 25

A file with no class declaration at all, just a void main() { ... }, is now real Java, not a beginner-only dialect:

void main() {
    var port = System.getenv("PORT");
    IO.println(port == null ? "PORT not set" : "listening on " + port);
}

That’s a complete, runnable .java file. It started as a preview in JEP 445 (21), got revised three more times across 22, 23, and 24, and finalized as JEP 512 in JDK 25 under a new name: compact source files. The IO class (basic println/readln) lives in java.lang, so it’s auto-imported everywhere, compact file or not — but its static methods aren’t auto-imported the way they were in early previews, so you still write IO.println(...), not a bare println(...). Compact files are designed to scale up cleanly — one that outgrows its single-method scope becomes an ordinary class with no rewrite needed, just wrapping class Name { ... } around what’s already there and adding back any explicit imports the auto-import was covering.

Constructors got a parallel simplification. JEP 513, also finalized in 25, lets statements run before a super(...) or this(...) call, as long as they don’t touch the object under construction yet:

class PositiveAmount {
    private final int cents;

    PositiveAmount(int cents) {
        if (cents < 0) throw new IllegalArgumentException("negative amount");
        this.cents = cents;
    }
}

class Refund extends PositiveAmount {
    Refund(int cents) {
        if (cents == 0) throw new IllegalArgumentException("refund can't be zero");
        super(cents); // validation now runs before the super call, not just after
    }
}

Before JEP 513 (previewed across three separate releases as JEP 447, 482, and 492), that early validation had to happen in a static helper method called inside the super(...) argument list — legal, but awkward enough that most people just skipped the validation instead.

The GC story: default coming for G1, real memory savings from compact headers

Generational ZGC shipped in 21, became the default ZGC mode in 23, and Generational Shenandoah finalized in 25. JDK 26 improved G1’s throughput by cutting synchronization overhead; JDK 27 finishes the arc with JEP 523, making G1 the default garbage collector in every environment, including the small-heap, single-core cases that used to fall back to Serial GC. Separately, compact object headers — merging the mark word and class pointer into a single 64-bit word instead of two — went from experimental in 24 to a finalized, opt-in product feature in 25 (JEP 519), then default-on starting with JDK 27 (JEP 534). JEP 519’s own benchmarks report 22% less heap use and 8% less CPU time on SPECjbb2015, and a 10% faster run on a parallel JSON-parsing benchmark — enable it with -XX:+UseCompactObjectHeaders on any JDK 25 or 26 install to get the win now, ahead of it becoming the default.

Still cooking: primitive patterns and structured concurrency

Not everything that previews finalizes on schedule. Primitive Types in Patterns, instanceof, and switch first previewed as JEP 455 in JDK 23; it’s on its fifth preview (JEP 532) in JDK 27, still not final. Structured Concurrency has previewed in every single release since JEP 453 in JDK 21 — seven previews (453, 462, 480, 499, 505, 525, 533) and counting, with no finalization JEP yet filed. Lazy Constants (renamed from Stable Values) is on its third preview in 27. None of these are close to landing behind a stable API; treat --enable-preview flags on them as exploratory, not as something to plan a JDK-upgrade migration around.

For a team sitting on 21 today, the practical delta by 25 is: virtual threads that no longer pin on synchronized, compact source files and flexible constructor bodies safe to use without a preview flag, and compact object headers worth turning on manually for the memory savings. The delta by 27 is G1 becoming the assumed default GC everywhere. Everything else on the list above is still a preview, exactly as it was two years ago.

🔗 Source: OpenJDK JEP Index