# Java 26 and the LTS Gap: What Piles Up While You Stay on 21

> Most production Java runs on 21, and will move next to 29. That means four releases of changes arriving in one upgrade. Java 26 shipped HTTP/3 in the standard HTTP client, faster startup, a free garbage collector improvement, and the first warnings in the plan to make final fields truly final. Here is what helps you, what will break your reflection-heavy libraries, and how to reduce the risk of an upgrade you have not started.

Author: Kishore K Sharma. Published: 2026-09-21. Canonical URL: https://kishorek.dev/writing/java-26-and-the-lts-gap. Tags: java, jvm, migration, backend, performance.
License: © 2026 Kishore K Sharma. All rights reserved. Reproduction requires attribution and a link to https://kishorek.dev/writing/java-26-and-the-lts-gap.

---
If you run Java in production, you are almost certainly on 21. It is the long-term support release most of the industry settled on, and the next one is 29. Java 26 arrived on 17 March 2026 with ten changes, is being replaced by 27 this month, and gets updates for six months in total.

So the real question is not "should we upgrade to 26". For most teams the answer is clearly no. The question is: **what is piling up between 21 and 29, and which parts will hurt when we take all of it in one jump?**

That framing matters, because moving from one long-term release to the next is not four small upgrades. It is one big one, where the individual changes were designed, deprecated and removed across releases you skipped. The warning that would have told you about a problem appeared in a Java version you never ran.

Java 26 is a good sample of that pile. Here is what is in it, sorted by whether it helps you or threatens you.

## The "final means final" plan has started warning

This is the item with real upgrade risk, and the one to read carefully.

**JEP 500: Prepare to Make Final Mean Final** adds warnings when code uses reflection to change a `final` field. Warnings only, in 26. The stated plan is a future release that blocks it completely.

Notice the shape of this. It is the same staged plan Java used before, when it locked down access to internal APIs: warn, then warn louder, then block by default with an escape flag, then remove the flag. Each stage lands in a different release. If you skip those releases, you only ever see the final state — an application that will not start.

![Java 21 on the left and Java 29 on the right, joined by a single arrow labelled 'one migration'. Below, the releases in between are drawn as a row: 22 to 25 marked unseen, 26 highlighted in danger colour and marked 'warns' with a note that JEP 500 raises its final-field warnings here, then 27 and 28 also marked unseen. A note on the right states that a team skipping those releases sees only the removal. The caption reads: a non-blocking CI job on a current Java version turns invisible debt into a list.](/writing/java-lts-gap-deprecations.svg "The warning that would have told you about a problem appears in a Java version you never ran.")

Why this matters more than it sounds: changing a `final` field through reflection is not an exotic trick. It is normal practice in

- libraries that convert objects to and from JSON or other formats,
- dependency injection frameworks that set fields directly,
- database mapping libraries that fill in entity objects,
- mocking libraries used in tests,
- test helpers that reset static configuration between test cases.

![Your own code sits on the left in accent colour, labelled "almost never". A divider separates it from four danger-coloured boxes on the right: JSON and serialization, dependency injection, database mapping, and mocking in tests — labelled "your dependencies, constantly". Below them a note states that you cannot fix these yourself, you wait for the maintainer. The caption reads: JEP 500 warns in Java 26 and blocks in a later release, so run your tests on a current JDK now because the warnings are the list.](/writing/jep500-final-field-reach.svg "You do not mutate final fields by reflection. Four categories of library you depend on do.")

You almost certainly do not do this in your own code. Your libraries almost certainly do.

The action you can take today, on 21, without changing your runtime: find out whether your stack depends on it. Run your test suite on a Java 26 build and collect the warnings. That gives you a list of libraries that need upgrading before the next long-term release. Doing this now, as a one-day check, is far cheaper than discovering it during an upgrade with a deadline — because the fix is often "wait for the library author", and you want that clock started early.

The reason Java is doing this at all is worth understanding rather than resenting. `final` is a promise the compiler relies on to optimise your code. If any field can be changed behind its back through reflection, the optimiser has to be cautious everywhere. Closing this gap is what makes a whole set of optimisations possible. The speed you get later is paid for by the flexibility you lose now.

## HTTP/3 in the standard client

**JEP 517** adds HTTP/3 support to the built-in `HttpClient`. Final, not a preview.

This is the change most likely to be immediately useful in normal backend work, because it removes a dependency instead of adding one. Until now, using HTTP/3 in Java meant pulling in a third-party client and everything it brings with it. Now it is part of the platform.

The reason to care is something called head-of-line blocking. HTTP/2 sends many requests over one TCP connection, and TCP guarantees data arrives in order — so one lost packet stalls **every** request sharing that connection until it is resent. HTTP/3 runs on QUIC over UDP, where requests are independent at the transport level. A lost packet stalls only the request it belonged to.

![Two stacked comparisons. The top shows HTTP/2 over TCP as one ordered byte stream: three requests A, B and C each send a row of packets, one of B's packets is marked lost in danger colour, and a vertical line shows all three rows stalling behind it until the lost packet is resent. The bottom shows HTTP/3 over QUIC with independent streams: the same packet of B is lost, but A and C continue arriving normally and only B stalls.](/writing/java26-head-of-line-blocking.svg "One lost packet stalls every HTTP/2 request sharing the connection. On HTTP/3 it stalls only the request it belonged to.")

Where this turns into real numbers: unreliable or high-latency networks, mobile clients, calls between regions, and any client making many requests to one server at the same time. On a clean network between two services in the same region, the difference is close to nothing — do not expect a win where there was no packet loss to begin with. I covered the transport details in [HTTP/3 and QUIC](/writing/http3-and-quic).

One operational warning that catches people: QUIC is UDP on port 443. Many corporate firewalls, older load balancers and default security group rules allow TCP on 443 but silently drop UDP. Clients usually fall back to HTTP/2, so the symptom is not an error — it is your carefully adopted HTTP/3 quietly never being used. Check which protocol was actually negotiated in production rather than assuming.

## Faster startup

**JEP 516: Ahead-of-Time Object Caching with Any GC** finds objects that are expensive to create during startup and caches them in advance. The important part is *with any GC* — earlier work on this only supported specific garbage collectors, which limited who could use it.

Startup time was a minor annoyance when a service ran for months on fixed servers. It is a direct cost when you scale up in response to traffic, run Java in a serverless function, or watch a rolling deploy crawl because each instance needs warming up before it can take load.

A useful way to see it: this is Java competing with the "just use Go" argument on the one point where Go's advantage was built-in rather than cultural. It does not close the gap completely. It narrows it, and it adds up with the other startup work arriving across these releases.

## G1 gets cheaper

**JEP 522: G1 GC — Improve Throughput by Reducing Synchronization.** G1 is the default garbage collector, so this is the rare change that helps you with no configuration and no code changes.

Less coordination work means less overhead on your application threads. Do not expect a headline number. Expect a few percent that adds up across a fleet. This category — improvements you get simply by upgrading — is the strongest argument for staying current.

## Structured concurrency, sixth preview

**JEP 525** is structured concurrency's *sixth* preview.

Read that number as information. It is the API most relevant to anyone using virtual threads. It lets you start several concurrent tasks and treat them as one unit, with sensible cancellation and error handling, instead of juggling a pile of futures. It is also an API that has been reshaped repeatedly across previews, which is exactly what previews are for.

My practical advice: learn it, build prototypes with it, do not put production code on it yet. Preview APIs can change in incompatible ways between releases, and an API on its sixth revision is telling you the design is still settling. If you already run virtual threads at scale — where the awkwardness is sharpest — follow it closely, because it is the missing half of the story I described in [Java 21 virtual threads in production](/writing/java-21-virtual-threads-in-production).

Alongside it, **JEP 526: Lazy Constants** is in its second preview. It gives you a supported way to delay creating a value while still letting the compiler treat it as a constant — a well-behaved replacement for the hand-written holder-class trick, and a natural fit with making `final` mean final.

## The rest, briefly

**JEP 530: Primitive Types in Patterns, `instanceof`, and `switch`** — fourth preview. Makes pattern matching work with primitive types everywhere it already works with objects. Removes an inconsistency every Java developer has tripped over. Still a preview, so still subject to change.

**JEP 524: PEM Encodings of Cryptographic Objects** — second preview. A standard way to read and write PEM files. Small, and quietly welcome if you have ever hand-parsed a certificate or pulled in a whole crypto library just to do it.

**JEP 529: Vector API** — eleventh incubator. Eleven. It has been incubating since Java 16 and is waiting on other JVM work before it can become final.

**JEP 504: Remove the Applet API** — deprecated in Java 17, gone in 26. Only relevant to a very old codebase, but if you have one, this is a hard failure rather than a warning.

## What to actually do this quarter

You are on 21. You are going to 29. Nobody is asking you to run 26 in production. Three things are still worth doing, in order of value:

**1. Run your test suite on a recent Java version in CI, as a non-blocking job.** One extra entry in your build matrix. It surfaces final-field warnings, removed-API failures and behaviour changes as a continuous signal instead of an upgrade-week surprise. This is by far the highest-value item here, and it costs an afternoon.

**2. List your reflection-heavy libraries.** Serialization, dependency injection, database mapping, mocking. For each one, check whether the maintainers have said anything about JEP 500. Libraries without a plan are your biggest risk, because you cannot fix them yourself.

**3. Decide your HTTP/3 position.** If you have mobile clients or cross-region service calls, HTTP/3 in the standard client is a genuine reason to want the next long-term release. Knowing that helps you justify the upgrade budget when you ask for it.

## The takeaway

Nobody is asking you to run Java 26 in production. It is not a long-term release, it is already being replaced, and your real next move is 21 to 29. But that jump is not four small upgrades. It is one big one, where the deprecations were spread across releases you never ran — so the warnings that would have warned you arrive as failures instead.

JEP 500 is the concrete example to act on now. Changing a `final` field through reflection is standard practice in serialization libraries, dependency injection frameworks, database mappers and mocking tools. Not in your code, but very much in your dependencies. The plan to forbid it has started with warnings.

The useful work this quarter is not a version bump. Add one non-blocking CI job running a current Java version against your existing tests, and read what it reports. That turns invisible debt into a list, and it costs an afternoon. The genuine wins in 26 — HTTP/3 in the standard client, faster startup, a free G1 improvement — are the argument you will use later to fund the upgrade. The CI job is what makes the upgrade survivable when you get it.
---

Originally published at https://kishorek.dev/writing/java-26-and-the-lts-gap.
© 2026 Kishore K Sharma. All rights reserved.
