Java LTS Evolution: From Java 8 to Java 25 — A Language Upgrade Guide
Back in high school I picked up Java on my own — Java 6 / 7 era. What I wrote was mostly Swing apps, and every time I wanted to pass a small function around I had to spell out a chunk of anonymous inner class. Looking back now it feels like building houses with Stone Age tools. In college I bumped into Java 8, wrote my first list.stream().filter(...).collect(...) one-liner, and the world cracked open: turns out Java can be written cleanly.
Since then a new LTS has dropped every two or three years, and somehow we are already at Java 25. This post walks the full Java 8 → 11 → 17 → 21 → 25 LTS line, what each version brought to the language, how long each JDK distribution will keep handing out free updates, and a quick stop at Java 26 — the non-LTS that just GA’d in March 2026.
Before diving in, a quick anchor on what “LTS” means and how Oracle JDK, OpenJDK, Eclipse Temurin, and Amazon Corretto actually differ. I covered this end-to-end in a separate post on JDK distributions; the short version is what I’ll lean on below: for free long-term support, pick a community / third-party distribution like Eclipse Temurin, Amazon Corretto, or Azul Zulu. Oracle JDK has been free under the NFTC license since 17, but only for about a year past the next LTS GA — not a fit for long-running production environments.
Why care about LTS at all
Starting with Java 9, Oracle moved to a six-month feature-release cadence. Short-cycle releases only get six months of security updates — once the next one ships, the old one is end-of-life. Fine for hobby projects, painful for enterprise. So in 2018 Oracle introduced the LTS (Long-Term Support) strategy: pick certain releases and back them with multi-year security updates. The OpenJDK community and every major distribution followed suit. LTS became the default answer for “which Java version should we run in prod?”
The LTS cadence started at three years (8 → 11 → 17 → 21), but starting from 21 it shortened to two years (21 → 25). The next LTS, 27, is scheduled for September 2027. The reason for the shorter cadence is industry pressure: features like virtual threads and pattern matching are too important to wait three years between LTS bumps, so the cycle got compressed.
Here are the five LTS releases with their support windows. I’m breaking out Oracle, Eclipse Temurin, and Amazon Corretto separately because their free-support periods diverge significantly:
| LTS | GA | Oracle Premier | Oracle Extended | Eclipse Temurin | Amazon Corretto |
|---|---|---|---|---|---|
| 8 | 2014-03 | 2022-03 (ended) | 2030-12 | 2030-12 | 2030-12 |
| 11 | 2018-09 | 2023-09 (ended) | 2032-01 | 2027-10 | 2032-01 |
| 17 | 2021-09 | 2026-09 | 2029-09 | 2027-10 | 2029-10 |
| 21 | 2023-09 | 2028-09 | 2031-09 | 2029-12 | 2030-10 |
| 25 | 2025-09-16 | 2030-09 | 2033-09 | 2031-09 | 2032-10 |
Important catch: Oracle JDK 17 / 21 / 25 are free for commercial use under the NFTC license, but that free window only lasts until “about one year after the next LTS GA.” For example, Java 21’s NFTC free window ends in 2026-09 — past that, Oracle JDK requires an Oracle Java SE Subscription. Corretto, Temurin, and other OpenJDK distributions are free for the entire support window; the only difference is how long that window is.
Java 8 — where it all started
Java 8 is the watershed of modern Java. It shipped in 2014, and as of 2026 it is still the most-deployed version. Part of that is enterprise upgrade inertia, but a big part is that it landed an enormous amount of useful stuff at once:
- Lambda expressions — collapsing chunky anonymous inner classes into single lines.
- Stream API — declarative collection operations, chainable, and parallelizable.
- java.time package — finally a replacement for the awkward
DateandCalendar. I wrote a walkthrough of the Java 8 date/time API. - Optional — letting “this might not have a value” be expressed in the type system.
- Default methods on interfaces — interfaces gain implementations, which is what made Stream API possible in the first place.
Here’s the canonical Lambda + Stream snippet that has worked unchanged from Java 8 through 25:
// Find names longer than 3 characters, uppercase them, sort, and print.
List<String> names = List.of("ada", "kyle", "tom", "alice");
names.stream()
.filter(n -> n.length() > 3) // lambda
.map(String::toUpperCase) // method reference
.sorted()
.forEach(System.out::println); // ALICE, KYLE
Why has Java 8 stuck around so long? The features themselves are good enough, the dependency graphs of enterprise systems are deep, the long tail of Spring 4 / 5 keeps it relevant, and Android only fully supported parts of Java 8 for years — all of which dilutes the upgrade incentive. Oracle Premier Support ended in 2022-03, but Eclipse Temurin and Amazon Corretto will keep shipping free updates until 2030-12, so legacy systems can keep eating Java 8 security patches without trouble.
Java 11 — the cleanup release that absorbed 9 and 10
Java 11 was the first LTS after Java 8, and it folded the features of two short-cycle releases (9 and 10) into long-term support.
What Java 11 itself brought
- JEP 321 — standardized HTTP Client. Previously incubator-only; with 11 it landed in
java.net.http, with HTTP/2 and async support. - Removed the Java EE and CORBA modules (those APIs were already obsolete).
- ZGC made its experimental debut.
- Single-file launch:
java HelloWorld.javaruns without an explicit compile step — handy for scripts. - TLS 1.3.
Carried over from Java 9 (2017-09)
- JEP 261 — Module System (Project Jigsaw). The JDK itself got modularized, and large applications can declare dependencies via
module-info.java. - JShell — the official REPL, great for learning and probing APIs.
- Immutable factory methods:
List.of(),Map.of(), etc. I wrote about the difference between Arrays.asList() and List.of(). - G1 became the default GC.
Carried over from Java 10 (2018-03)
- JEP 286 — local-variable type inference with
var. Code likevar list = new ArrayList<String>();saves a few keystrokes. - Parallel full GC for G1.
The Java 11 absorption was only a partial win. The Module System is powerful, but adoption has been slow — frameworks like Spring and Hibernate kept compatibility surfaces wide and never fully modularized, and developers upgrading to 11 often got bitten by the new restrictions on reflection and JDK-internal API access. Oracle Premier Support ended in 2023-09, but Corretto’s free window runs to 2032-01, so staying on Java 11 is still safe for now.
Java 17 — modern Java takes shape
Java 17 is where “modern Java” really emerged, by promoting a large pile of preview features from 12-16 into standard.
What Java 17 itself brought
- JEP 409 — sealed classes. Lock down which classes can extend a given class — the foundation, together with pattern matching, for ADT (algebraic data type) style code. I covered this in detail in Java 17 sealed classes explained.
- JEP 403 — strong encapsulation of JDK internals.
sun.*,com.sun.*, and friends are no longer accessible via reflection by default — a change that broke a lot of older libraries on upgrade. - Enhanced Pseudo-Random Number Generators.
- New macOS rendering pipeline (better story for Apple Silicon).
Carried forward from 12-16
- 14: JEP 361 — switch expressions as a standard feature. switch can finally be used as an expression and yield values. The full evolution of switch is its own post on the switch keyword. Also: helpful NullPointerExceptions (the message tells you which variable was null).
- 15: Text blocks went standard — triple-quoted multi-line strings. ZGC and Shenandoah moved out of experimental.
- 16: JEP 395 — records as a standard feature. One-line declaration of immutable data classes. Trade-offs versus Lombok in Java 16 record tutorial: how record, Lombok, and POJO compare. Also JEP 394 — pattern matching for instanceof as a standard feature, plus Unix-domain sockets.
By Java 17 the modern Java vocabulary now included sealed, record, pattern matching, text blocks, switch expressions — syntactically two generations removed from Java 8. Here’s the switch expression form:
// Switch as an expression — returns a value directly.
String role = switch (level) {
case 1, 2 -> "junior"; // multiple cases joined
case 3, 4, 5 -> "mid";
case 6, 7 -> "senior";
default -> throw new IllegalArgumentException("unknown level: " + level);
};
Text blocks (Java 15, mainstreamed in 17) are the other syntax change that finally killed string concatenation when writing SQL, JSON, or HTML:
// Triple-quote opening; indentation is normalized to the least-indented line.
String json = """
{
"name": "kyle",
"role": "engineer",
"skills": ["Java", "Spring", "Linux"]
}
""";
// SQL gets much cleaner too.
String sql = """
SELECT id, name, created_at
FROM users
WHERE status = 'active'
AND created_at > ?
ORDER BY created_at DESC
""";
Support: Oracle Premier through 2026-09, Eclipse Temurin through 2027-10, Corretto through 2029-10.
Java 21 — virtual threads year zero, absorbing 18-20
Java 21 is the heaviest release of the recent past — 15 JEPs, the marquee one being virtual threads finally graduating from preview to standard.
What Java 21 itself brought
- JEP 444 — virtual threads as a standard feature. For high-concurrency IO services (HTTP backends, database-heavy code) this is era-defining. The throughput you used to need reactive programming for is now reachable with plain synchronous code.
- JEP 431 — sequenced collections. Adds a unified
getFirst(),getLast(),reversed()interface to List, Deque, LinkedHashSet, and friends. - JEP 441 — pattern matching for switch as standard. switch can match on type and on record structure.
- JEP 440 — record patterns as standard. Destructure record fields directly in patterns.
- Generational ZGC — ZGC gains generational regions for better throughput.
- KEM API — paving the way for post-quantum cryptography.
Carried forward from 18-20
- 18: JEP 400 — UTF-8 as the default charset. Easy to overlook, hugely impactful:
new FileWriter("a.txt")used to use different encodings on different platforms and locales; from 18 onward it is finally consistent. Also the simple web server (jwebserver). - 19: First preview of virtual threads, structured concurrency entered incubation.
- 20: Scoped values entered incubation, virtual threads got their second preview round.
Here’s a virtual-threads example. The code looks just like ordinary thread code, but you can spin up millions of these without blowing memory:
// Virtual-thread executor running 10,000 concurrent IO tasks.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
// Simulated IO — an HTTP call, a DB query, etc.
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
} // executor auto-closes; awaits all tasks.
Run that with traditional platform threads and 10,000 threads will crush the JVM. With virtual threads, each task’s stack is a few KB and the program finishes comfortably. For people used to writing reactive code, this feels like “we can finally just write synchronous code again.”
JEP 441 brought pattern matching to switch. Combined with sealed classes and records, you can write proper ADT-style branching — no more layers of if-else plus instanceof casts:
// A sealed hierarchy — every Shape implementer is enumerated.
sealed interface Shape permits Circle, Square, Triangle {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
record Triangle(double base, double height) implements Shape {}
// switch matches on type directly; the compiler verifies exhaustiveness.
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
case Triangle t -> 0.5 * t.base() * t.height();
// No default needed — sealed + full enumeration lets the compiler guarantee it.
};
JEP 440 record patterns go a step further by destructuring record fields directly, removing piles of getter calls:
record Point(int x, int y) {}
record Line(Point from, Point to) {}
// Destructure records — including nested records — straight inside switch cases.
String describe = switch (obj) {
case Point(int x, int y) -> "Point (" + x + "," + y + ")";
case Line(Point(var x1, var y1),
Point(var x2, var y2)) -> "Line " + x1 + "," + y1 + " -> " + x2 + "," + y2;
case null -> "null";
default -> "unknown shape";
};
Java 21 is also the inflection point for the LTS cadence — from 21 onward, the next LTS is two years out (25), not three (24). Support: Oracle Premier through 2028-09, Temurin through 2029-12.
Java 25 — the current LTS, absorbing 22-24
Java 25 went GA in September 2025 — the latest LTS at the time of writing. 18 JEPs in total, focused on promoting features accumulated over the two years since 21 and patching a few long-standing rough edges.
What Java 25 itself brought
- JEP 512 — compact source files & instance main. Write
void main()and run it — no morepublic static void main(String[] args)boilerplate. Targeted at teaching and one-off scripts. - JEP 513 — flexible constructor bodies. Constructors can now do work (validation, preprocessing) before the
super()call, lifting the constraint that the super-call had to be the first statement. - JEP 519 — compact object headers. Object headers shrink from 96-128 bits down to 64 bits — a free heap-memory win for large applications.
- JEP 510 — KDF API, JEP 509 — JFR CPU-time profiling.
- Module import declarations, PEM API, generational Shenandoah as standard.
- 32-bit x86 support removed.
Carried forward from 22-24
- 22: JEP 454 — FFM API as standard (Foreign Function & Memory — the modern foreign-call API replacing JNI). Unnamed patterns standard, stream gatherers in preview, multi-file source launch.
- 23: ZGC defaults to generational, Markdown Javadoc.
- 24: JEP 485 — stream gatherers as standard (the biggest Stream API expansion since Java 8 — custom intermediate operations). JEP 484 — Class-File API as standard. JEP 483 AOT class loading. Security Manager permanently disabled. ML-KEM / ML-DSA post-quantum crypto. JEP 491 — virtual thread synchronization without pinning (fixes the Java 21 footgun where
synchronizedblocks pinned a virtual thread to its underlying carrier thread).
Here’s the Java 25 instance-main form, plus the stream-gatherers example that became standard in Java 24:
// JEP 512 compact source files — the entire file is these four lines, saved as Hello.java.
// No class declaration, no public static void main, no imports needed.
void main() {
println("Hello from Java 25"); // println is implicitly available — equivalent to System.out.println.
}
// You can still take command-line args; just declare them when you need them.
void main(String[] args) {
println("args count: " + args.length);
}
The real value of JEP 512 isn’t keystrokes saved — it’s lowering the bar for newcomers. The first lesson no longer has to explain public, static, or String[] args before getting to actual logic. And for one-off scripts, java Hello.java runs directly.
The other Java 24 standardization worth a look is stream gatherers, the largest Stream API expansion since Java 8 — they let you define custom intermediate operations (previously you could only compose the built-in filter / map / flatMap / etc.):
import static java.util.stream.Gatherers.windowSliding;
import static java.util.stream.Gatherers.windowFixed;
// Sliding window — every N consecutive elements, advancing by one each step.
var pairs = Stream.of(1, 2, 3, 4, 5)
.gather(windowSliding(2))
.toList();
// [[1,2], [2,3], [3,4], [4,5]]
// Fixed window — non-overlapping chunks, useful for batch processing.
var batches = Stream.of(1, 2, 3, 4, 5, 6, 7)
.gather(windowFixed(3))
.toList();
// [[1,2,3], [4,5,6], [7]]
Across the two years from 21 to 25, Java rounded out virtual threads (no more pinning), expanded Stream (gatherers), and shrank the heap (compact headers). The “modern, high-performance Java” line is now complete. Support: Oracle Premier through 2030-09, Temurin through 2031-09, Corretto through 2032-10.
Aside: Java 26 — non-LTS, but the latest
Java 26 went GA on March 17, 2026. It’s not LTS (only six months of security updates), but as the latest at the time of writing, a few highlights are worth tracking — most of these will likely standardize in the next LTS, 27:
- JEP 517 — HTTP/3 for HTTP Client (standard). The HTTP Client standardized in Java 11 finally supports HTTP/3 and QUIC, putting the network stack on par with modern browsers.
- JEP 516 — AOT object caching with any GC (standard). A continuation of Project Leyden — startup time keeps improving.
- JEP 522 — G1 GC throughput improvement (standard). Lower G1 synchronization overhead — a free win for any service already on G1.
- JEP 504 — Applet API removed (standard).
java.appletfinally laid to rest. - JEP 500 — Prepare to Make Final Mean Final. Tightens the reflection loophole for modifying
finalfields — paving the way for actually-final final. - JEP 525 — Structured Concurrency 6th preview. Has been in preview since 21; the most important post-Loom concurrency API still in flight.
- JEP 530 — Primitive types in patterns 4th preview.
- JEP 529 — Vector API 11th incubator round. Still incubating.
Java 26 has no headline feature — mostly continued progress on previews from 25, plus practical additions like HTTP/3 and G1 tuning. Production should stay on the 25 LTS, but the HTTP/3 client is fun to play with on its own.
Should you upgrade? And to what version?
There are still plenty of places running Java 8 — financial systems, legacy Spring services, old Android projects, Scala 2 / Kotlin 1 apps on the JVM. Not all of them need to move immediately. Java 8 has free Eclipse Temurin and Amazon Corretto support through 2030-12, so security patches are covered. But if your business starts wanting virtual threads, records, or pattern matching, it’s time to schedule the upgrade.
When you do upgrade, here’s the rule: don’t stop in the middle at 11 or 17 — jump straight to 21 or 25. The cost of upgrading is mostly in the chores — dealing with reflection and JDK-internal API access being blocked, updating dependencies, fixing CI — and doing it once costs about the same as doing it twice. If you’re going to move, go all the way.
- Jump to Java 21: stable enough by now (over two years GA), virtual threads pay off immediately for IO-bound services, Spring Boot 3 supports it cleanly.
- Jump to Java 25: Java 21’s virtual-thread pinning issue is fixed (JEP 491), stream gatherers make data-processing code cleaner, compact object headers save memory. New projects should start on 25 directly.
Framework compatibility is also a factor: Spring Boot 3 requires Java 17+, and Spring Boot 4 is expected to require 21+. So any service planning to move to Spring Boot 4 within the next 12 months needs to plan for at least 21.
FAQ
Q1: Who decides what’s an LTS? Can I use the non-LTS releases?
LTS is an Oracle policy from 2018 that the OpenJDK community and major distributions (Temurin, Corretto, Zulu) followed. Non-LTS releases (22, 23, 24, 26, etc.) get only six months of security updates — fine for experimentation, hobby projects, or anything with a short lifespan, but not for long-running services. They’re perfectly fine for development, demos, and trying out new features.
Q2: What’s the difference between free OpenJDK distributions and Oracle JDK?
Functionally, almost none — Oracle JDK and OpenJDK share the same source base. The differences are mostly licensing and support length; full breakdown in which OpenJDK distribution should you use. Bottom line: for free long-term support, use Temurin, Corretto, or Zulu — don’t run Oracle JDK in production unless you have an Oracle Java SE Subscription.
Q3: What are the common upgrade landmines?
- Java 8 → 11: the Module System (JPMS) blocks a lot of reflection and JDK-internal API access, breaking older libraries.
- Java 11 → 17: JEP 403 strong-encapsulates JDK internals — even more reflection-using code breaks. Remember
--add-opens, or upgrade to libraries that target 17. - Java 17 → 21: relatively smooth, as long as you weren’t relying on deprecated APIs.
- Java 21 → 25: watch out for 32-bit x86 removal, the permanent disabling of the Security Manager, and a handful of niche API removals.
Q4: Were virtual threads introduced in Java 21?
Yes, but they preview-debuted in Java 19 and shipped standard in 21. Java 25 finished the job (JEP 491 fixes the synchronized-block pinning of carrier threads). For IO-bound services (HTTP backends, database-heavy code), virtual threads are the most tangible upgrade reason of the past several years.
Q5: Java 21 or Java 25 — which should I pick?
New projects, Java 25 directly. For existing projects that are stable on 21, you can wait six months to a year for 25’s patch releases to settle, then upgrade. Java 25’s free-support window is longer (Corretto through 2032-10), so it’s the better long-term bet.
Q6: Can the JDK’s built-in HTTP Client really replace OkHttp or Apache HttpClient?
Java 11’s standardized HTTP Client (JEP 321) supports HTTP/2, async, and reactive; Java 26 added HTTP/3. For everyday REST calls, it’s more than enough. But on the ecosystem side — interceptors, metrics, retry policies, auth packages — OkHttp and Apache are still ahead. For complex integrations you may still reach for a third-party client.
Q7: Is the LTS cadence shortening to two years a good thing?
For individual developers — yes. Virtual threads went from preview in 19 to standard in 21 in just two years; new features land in LTS faster, and frameworks and ecosystems can move faster too. For enterprises — more pressure: tighter upgrade cycles, more budget and test capacity required. Overall, the faster cadence is necessary for Java to keep up with fast-moving languages like Go, Rust, and Kotlin.
Wrap-up
Three takeaways:
- Across the eleven years from Java 8 to Java 25, the language went from primarily OOP to a full “OOP + functional + pattern matching + new concurrency model” stack. Modern Java’s vocabulary is genuinely two generations removed from Java 8.
- LTS is the safety net for picking enterprise Java versions, but remember Oracle JDK and OpenJDK distributions have very different free-support windows. For long-running production, pick Eclipse Temurin, Amazon Corretto, or Azul Zulu.
- Services still on Java 8 / 11 thinking about upgrading should jump straight to 21 or 25 — stopping at 17 doesn’t pay off anymore. New projects with no baggage should pick 25.
From writing anonymous-inner-class Java in high school on Java 6 / 7 to writing virtual thread + record pattern Java 25 today, the timeline really does show a language that’s still alive. Java has never been the most beautiful language to write, but the direction it’s evolved in has stayed consistently practical — picking off the actual pain points developers hit. The next LTS, 27, is targeted for September 2027 — by then we’ll likely see structured concurrency, primitive patterns, and string templates promoted out of preview. Looking forward to it.
References: