Connection · Interrupted

Something didn't load

Part of this page failed to reach you. Reload to try again — if it keeps happening, check your connection.

Skip to main content
Engineering9 min read

Java 26 Landed: The Features to Track Before the Java 29 LTS

Most backend teams run only the LTS lines, so Java 26 looks skippable. It is not — it is the clearest preview of what the Java 29 LTS will contain. These are my notes on what is already final (AOT object caching with ZGC, a faster G1, HTTP/3), the one preview I would rewrite code for (Lazy Constants, with a runnable example), and the integrity change that will break builds.

All Posts
2/4

Java 26 reached general availability on 17 March 2026 with ten JEPs (JDK 26 project page). If you deploy on the six-month feature releases, you already know the drill. If, like most backend shops I have worked near, you only run the LTS lines, it is tempting to skip this one and wait for the next long-term release.

That is the wrong instinct, and here is why. The next LTS is Java 29, due September 2027. Between now and then sit exactly three feature releases: 26, 27, and 28. Nothing lands final in an LTS that has not already matured across those interim releases. So Java 26 is not a release you have to adopt. It is the earliest honest preview of what your Java 29 baseline will contain. Reading it now is how you avoid being surprised by a finalized API — or a broken build — eighteen months from now.

I read all ten JEPs against the primary sources. Below is the backend engineer's cut: what is already final and therefore guaranteed for 29, the one preview that changed how I write code, the previews worth tracking, and the change most likely to break your build.

Three features that are already final

A finalized feature in 26 is a feature you can count on in 29. Three of them matter for server-side work.

AOT object caching now works with any garbage collector (JEP 516). This is Project Leyden's startup work, and the headline is that the ahead-of-time cache is no longer tied to a specific GC. Before 26, the cached-object format was mapped into memory in a GC-specific layout, which excluded ZGC — so you had to choose between ZGC's sub-millisecond pauses and Leyden's faster startup (JEP 516). Java 26 stores cached objects in a GC-agnostic format, as logical indices rather than direct references, and a background thread streams them into the heap at startup. You opt in with -XX:+AOTStreamableObjects. For anything that starts often — serverless functions, autoscaled pods, CI workers — this removes the reason ZGC users had to skip AOT caching. That combination is the one I expect to become a default recommendation by the time 29 ships.

G1 got measurably faster by doing less synchronization (JEP 522). This is the most concrete win in the release. G1 introduced a second card table so application threads always write to the "active" table without coordinating with G1's background refinement threads. The result: memory synchronization between the collector and application threads drops to roughly the level of Serial and Parallel GC. The numbers from the JEP are worth quoting — reference-heavy workloads see 5–15% throughput gains, lighter ones around 5%, and on x64 the write barrier shrinks from about 50 instructions to about 12 (JEP 522). It is not free: each card table costs 0.2% of heap capacity, about 2 MB of native memory per 1 GB of heap. For a service that spends real CPU in the write barrier, that is a rounding-error price for a double-digit throughput bump, and G1 is still the default collector — so most services inherit this without changing a flag.

HTTP/3 landed in the HTTP Client (JEP 517). The java.net.http client can now speak HTTP/3 over QUIC, no third-party library required (JEP 517). If you make outbound calls to services or gateways that support HTTP/3, you get head-of-line-blocking avoidance at the transport layer from the standard client. It is a request-configuration change, not a rewrite. I would not migrate anything on day one, but by 29 this is the baseline HTTP client, and knowing it exists changes how you evaluate a slow upstream.

Lazy Constants: the preview I would rewrite code for

The feature that changed how I think about initialization is Lazy Constants (JEP 526), a second preview in 26. It replaces a pattern every JVM engineer has written badly at least once: the thread-safe lazily-initialized singleton, usually via double-checked locking with a volatile field and a synchronized block.

LazyConstant<T> is a container that computes its value at most once, on first access, through a Supplier, and then holds it as an effectively final value. Because the value settles into a @Stable field, the JIT can constant-fold it after initialization — so unlike a hand-rolled holder, you pay the deferred-initialization tax exactly once and the reads afterward can be as cheap as reading a real constant (JEP 526).

Here is the whole pattern in one runnable file. Thirty-two virtual threads race to read the value before it exists; the point is that the supplier runs exactly once.

java
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

public class LazyDemo {

    // One field replaces the whole double-checked-locking singleton dance.
    private static final LazyConstant<Config> CONFIG = LazyConstant.of(LazyDemo::loadConfig);

    static final AtomicInteger supplierRuns = new AtomicInteger();

    record Config(int connections, String region) {}

    // An expensive initializer to run at most once, on first use.
    static Config loadConfig() {
        supplierRuns.incrementAndGet();
        try {
            Thread.sleep(50); // stand in for reading a file or opening a pool
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return new Config(64, "eu-west-1");
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("before first get: initialized=" + CONFIG.isInitialized());

        // 32 threads race to read the constant before it is initialized.
        var racers = IntStream.range(0, 32)
                .mapToObj(i -> Thread.ofVirtual().unstarted(() -> {
                    Config c = CONFIG.get(); // first caller initializes; the rest reuse
                    if (c.connections() != 64) {
                        throw new AssertionError("torn read");
                    }
                }))
                .toList();

        racers.forEach(Thread::start);
        for (Thread t : racers) {
            t.join();
        }

        System.out.println("after 32 racers: initialized=" + CONFIG.isInitialized());
        System.out.println("supplier ran " + supplierRuns.get() + " time(s)");
        System.out.println("region=" + CONFIG.get().region());
    }
}

Lazy Constants is a preview API, so compile and run with the preview flag:

javac --release 26 --enable-preview LazyDemo.java java --enable-preview LazyDemo

On JDK 26.0.1 this prints initialized=false, then after the race initialized=true, supplier ran 1 time(s), and region=eu-west-1. That single line — the supplier ran once despite 32 concurrent first-readers — is the entire correctness guarantee you used to hand-write and hope you got right.

Two lines deserve attention. LazyConstant.of(LazyDemo::loadConfig) binds the supplier without running it; nothing is computed until the first get(). And CONFIG.isInitialized() lets you check state without forcing initialization, which is useful for health checks that should not trigger the very work they are probing. The API also covers collections: List.ofLazy(size, indexFn) and Map.ofLazy(keys, valueFn) give you element-wise lazy structures where each slot initializes independently on first touch — a memoized lookup table without a concurrent map and its per-read overhead.

The caveat is the preview status itself. The API surface can shift before it finalizes, and I would not scatter it across a large codebase yet. I have it in a throwaway branch to feel the ergonomics. That caution is already earned: JEP 531 has since targeted a third preview for Java 27 that drops isInitialized() and orElse and adds Set.ofLazy — so the isInitialized() call above still compiles on 26 but is already one of the methods on its way out (JEP 531). By the time 29 arrives this is still how I will write lazy initialization — and double-checked locking becomes a code smell rather than a necessary evil.

The previews worth tracking, not yet adopting

Two more previews shape what 29 will feel like.

Primitive types in patterns, instanceof, and switch reached its fourth preview (JEP 530). It lets pattern matching work directly on primitives — switch over an int with range-style patterns, instanceof narrowing to a primitive — closing the gap that made pattern matching feel like a reference-types-only feature. Four previews deep, it is close to done, and it is the kind of language change that quietly removes a category of boilerplate once it is everywhere.

Structured concurrency reached its sixth preview (JEP 525). It has been the slowest of the modern concurrency features to settle, and the API has churned across previews. I wrote up how its failure and cancellation semantics compare across runtimes in a separate post, so I will not re-explain it here — the relevant point for planning is that six previews signals an API still finding its final shape, so I would treat anything you build on it today as provisional until it finalizes, plausibly in 29.

The Vector API remains in incubation, its eleventh round (JEP 529). It has incubated for years waiting on Project Valhalla, and there is no finalization on the horizon. Track it if you do numeric or SIMD-heavy work; do not plan around it landing in 29.

What will break: final starts to mean final

The change most likely to touch your build is not a feature at all. "Prepare to Make Final Mean Final" (JEP 500) is the integrity-by-default work: the JDK now warns when code uses deep reflection to mutate fields declared final (JDK 26 features). For years, serialization libraries, mocking frameworks, and dependency-injection containers have reached in and rewritten final fields. Java 26 warns about it; a future release will deny it by default. If your test suite or startup logs light up with warnings about final-field mutation, that is a dependency doing something that will stop working — better to learn which one now than during a Java 29 migration under deadline.

The other removal is smaller: the Applet API is gone (JEP 504). It has been deprecated for years and unusable in modern browsers for longer. If anything you own still imports java.applet, 26 is where it stops compiling. I mention it only because "removed" is the one status that turns a warning into a failure.

What to do now if you live on LTS

You do not need to run Java 26 in production to benefit from it. You need to read it as a forecast.

  • Trial the AOT object cache with ZGC (-XX:+AOTStreamableObjects) on a startup-sensitive service and note the numbers — this pairing is a strong candidate to become a default recommendation by 29.
  • Expect the G1 throughput win for free when you eventually move to 29; if you profile hot write barriers today, that 5–15% is real headroom already banked.
  • Prototype Lazy Constants in a branch to retire a double-checked-locking singleton, but keep it out of shared code until the preview finalizes.
  • Run your existing test suite on a 26 JDK now and grep the logs for final-field-mutation warnings. Every warning is a dependency you will have to upgrade or replace before 29.
  • Treat structured concurrency and primitive patterns as "coming, not here" — worth reading, not worth betting a design on until they finalize.

Java 26 is a short-term release with a six-month support window, so most teams will never deploy it. That is fine. Its real value is as a rehearsal: the finalized features are your Java 29 floor, the previews are its likely ceiling, and the integrity warnings are your migration to-do list, available a year and a half early.

Read next

Still here? You might enjoy this.

Nothing close enough — try a different angle?

Was this helpful?

Leave a rating or a quick note — it helps me improve.