Java 8 to Java 21: Modernizing the JVM

ModernLift · ·11 min read
Part 2 of 9

Upgrading from Java 8 to Java 21 means moving from a frozen runtime — Oracle ended free public JDK 8 updates in January 2019 (Oracle's Java SE Support Roadmap) — to the current long-term-support release, Java 21 (September 2023). It is rarely a recompile: removed APIs, the strong encapsulation of internal JDK classes, the module system, and updated third-party libraries all surface real breakage. The reliable path is to step through the LTS releases (8 to 11 to 17 to 21), fix what each surfaces, and validate behavior against the old runtime before cutting production traffic over.

Part 1 named four entangled layers in a legacy Java estate. This part takes the one with a clock on it: the runtime. Of all the work in a Java modernization, upgrading the JDK is usually first — because an unsupported runtime is where the security and audit pressure is sharpest, and because the framework move in Part 3 needs a modern JDK underneath it anyway. Get the runtime current and you have cleared the most urgent risk; sometimes it is the only move a stable application needs.

Where Java 8 stands, and why it won’t sit still

The Java platform retires on a published schedule, and the versions common in enterprise production sit at very different points on it.

ReleaseLTS?Status
Java 8YesFree public updates ended Jan 2019; Premier Support ended Mar 2022; paid Extended Support to Dec 2030
Java 11YesCurrent LTS, but a stepping stone — newer LTS releases supersede it
Java 17YesCurrent LTS (Sept 2021) — the first under the two-year LTS cadence
Java 21YesNewest LTS (Sept 2023) — the longest support runway ahead of it

Dates are from Oracle’s Java SE Support Roadmap. Two facts shape the decision. First, Java 8 is frozen: without a paid subscription it receives no security fixes, and in a regulated environment an unpatchable runtime in scope is an audit finding waiting to happen — the same logic the end-of-life software risks part lays out for any unsupported platform. Second, since Java 17 the JDK has shipped a long-term-support release every two years, on top of a feature release every six months. The practical reading: land on a current LTS and stay close to it, so the next upgrade is a step, not another decade-long drift.

Why this is not a recompile

The dangerous assumption is that moving from Java 8 to Java 21 means bumping a version number and rebuilding. It rarely is, and the reasons are worth knowing before you scope the work.

  • Removed and repackaged APIs. Java 11 removed pieces that used to ship with the JDK — Java EE modules (JAXB, JAX-WS) and CORBA among them. Code that relied on them no longer compiles until those dependencies are added back explicitly. This is also where the runtime upgrade starts to touch the framework layer, because some of what was removed is Java EE.
  • Strong encapsulation of internals. The module system (introduced in Java 9) progressively locked down internal JDK classes such as sun.misc.Unsafe. Libraries that reached into those internals — and many older ones did — break or warn until upgraded.
  • Third-party dependencies. Older versions of common libraries and build plugins often do not run on a modern JDK. The upgrade pulls a chain of dependency bumps with it, each carrying its own behavior changes.
  • Behavior, not just compilation. The riskiest changes are the ones that compile cleanly and behave differently — garbage-collector defaults, date/time formatting, or floating-point and locale subtleties. These do not announce themselves at build time. They show up in production unless you test for them.

That last point is the whole argument for doing this carefully: a runtime upgrade can pass the compiler and still change what the application does.

The path: step through the LTS releases

The reliable upgrade is not one leap but a sequence of LTS hops — 8 to 11, 11 to 17, 17 to 21 — fixing what each step surfaces before taking the next. Stepping through is easier to reason about than jumping the whole distance, because each release’s breaking changes are documented relative to the one before, and you isolate which change caused which failure.

At each step the loop is the same: bump the target JDK, resolve the compile and dependency breakage, run the suite, and — crucially — compare behavior against the prior runtime. A modern toolchain helps here. The JDK ships a migration analysis tool (jdeps) that flags dependencies on removed and internal APIs before you run anything, and AI-accelerated discovery can read the codebase and its dependency graph to surface where the removed APIs and reflective access actually live. That turns “rebuild and hope” into a mapped piece of work — covered in Part 7, Java Modernization with AI.

Validating the upgrade — the part teams skip

Because the dangerous changes are behavioral, a runtime upgrade has to be proven, not assumed. This is where the series’ central discipline applies even to “just an upgrade”: prove functional equivalence between the application running on the old JDK and on the new one.

The most effective way is shadow traffic: run the upgraded build alongside the existing one, send both the same real requests, and reconcile the outputs. Where they diverge, you have found a behavior change — a different rounding, a changed sort order, a serialization difference — before it reached a customer. Only once the new runtime matches does production traffic shift over, gradually, with rollback one flag away.

The reversal worth keeping: a JDK upgrade that compiles is not a JDK upgrade that works — equivalence is something you demonstrate, not something the build tells you.

Sometimes it really is close to a recompile

For a well-contained, portable application whose framework already supports a modern JDK, the runtime upgrade can be genuinely straightforward — close to the recompile people hope for — and it is the lowest-cost way to clear the end-of-life risk. The job gets larger when the code is bound to APIs the JDK removed, when libraries reach into JDK internals, or when the framework generation itself is too old to run on a current JDK. In that last case the runtime upgrade and the framework migration stop being separable — which is exactly the entanglement Part 1 described, and the subject of the next part.

Where this leads

Upgrading the JDK clears the support clock, but it often runs straight into the next wall: a framework generation too old to move forward. The Java EE APIs the application is written against — and the javax.* namespace they live in — are frozen, and a modern JDK only sharpens the pressure to leave them. Part 3, Java EE to Spring Boot & Jakarta EE, covers the framework move: what the javax.* to jakarta.* break actually means, the choice between Jakarta EE and Spring Boot, and how to cross that line one slice at a time.

Frequently asked questions

Is Java 8 still supported?
Oracle ended free public updates for JDK 8 in January 2019 and Premier Support in March 2022 (Oracle's Java SE Support Roadmap); security patches now require a commercial subscription, with paid Extended Support running to December 2030. Many estates also run unsupported community builds. Either way, Java 8 is a frozen platform — it gets no new features and, without a paid subscription, no security fixes — which is why it is the runtime most enterprises need to move off first.
Why upgrade straight to Java 21 rather than 11 or 17?
Java 21 is the newest long-term-support release (September 2023), so it has the longest support runway ahead of it — landing on it means you upgrade less often. Java 11 and 17 are also LTS and entirely valid stepping stones, and most teams move through them rather than in one jump, fixing what each surfaces. The aim is to land on a current LTS and stay close to it, not to chase every six-month feature release.
What breaks when upgrading from Java 8 to a modern JDK?
More than a recompile usually reveals. Removed and repackaged APIs (parts of Java EE and CORBA were removed from the JDK in Java 11), the strong encapsulation of internal classes that some libraries reached into, the module system, and third-party dependencies that need newer versions all surface real breakage. None of it is insurmountable, but it is why an upgrade needs proper validation rather than a confident rebuild — the failures are often subtle behavior changes, not compile errors.
All 9 parts of Java EE & Legacy Java Modernization →