Java's Four-Generation Leap: From Lambdas to Virtual Threads
JDK 8 → 17 → 21 → 25: A Clear Explanation of the Key Leaps Across Four Generations
1. Version Positioning and Release Strategy
First, let's clarify the positioning of the four versions:
The change in release cadence is key to understanding the Java ecosystem:
- Before JDK 8: Major versions were 2-3 years apart, feature accumulation was severe, and delays were the norm (JDK 9 was delayed by three years).
- After JDK 9: Switched to a 6-month train model, with releases in March and September each year.
- LTS cadence: Initially an LTS every 3 years (8 → 11 → 17), starting from JDK 17 it changed to every 2 years (17 → 21 → 25).
This strategic change directly impacts our technology choices. Many teams stayed on JDK 8 for nearly a decade, the core reason being that there was no LTS version between JDK 9 and JDK 16 worth the risk of upgrading. JDK 17 was the first version that truly made everyone feel "it's time to upgrade."
A couple of words on licensing: After JDK 8u211, commercial use of Oracle JDK requires a fee. Starting with JDK 17, Oracle reinstated the NFTC (No-Fee Terms and Conditions) license, allowing free commercial use. However, many companies have already switched to distributions like Adoptium (Eclipse Temurin), Amazon Corretto, and Azul Zulu, which are free across all versions.
2. Language Feature Evolution
2.1 JDK 8: The Functional Revolution of Lambda and Stream
JDK 8 is the most significant language-level change in Java's history. It introduced four core features—Lambda, Stream, Optional, and the new Date API—all at once, directly changing Java's programming paradigm.
Lambda Expressions and Functional Interfaces
Writing a thread sort before JDK 8:
After JDK 8:
Lambda's underlying implementation is worth understanding: it is not syntactic sugar for anonymous inner classes. The compiler uses the invokedynamic instruction to generate implementation classes at runtime via LambdaMetafactory, avoiding the problem of anonymous inner classes generating a large number of .class files at compile time. You can see this difference using javap -c -p:
Stream API
The core design of Stream is lazy evaluation. Intermediate operations (filter, map, flatMap) are not executed immediately; the entire pipeline is processed only when a terminal operation (collect, forEach, reduce) is triggered:
Regarding parallel streams, my advice is: rarely use them in web applications. Parallel streams use ForkJoinPool.commonPool() under the hood, which is a globally shared thread pool. If you use a parallel stream for a time-consuming operation within one HTTP request handler, it will affect the execution of parallel streams in other requests. We once encountered interface timeouts in production because of this.
The diagram above shows the complete processing flow of a Stream pipeline—from the data source to intermediate operations (lazy) to terminal operations (triggering execution), as well as the Fork-Join divide-and-conquer model of parallel streams.
Optional
java.time API
Finally replaced the disastrous java.util.Date and Calendar:
2.2 Key Transitional Features of JDK 9-16
Many people skip this section entirely, but the transitional features of JDK 9-16 were all finalized in subsequent LTS versions. Without understanding their evolution, you'll feel these features "appeared out of nowhere" when using JDK 17/21.
JDK 9: Module System (Project Jigsaw)
JDK 9 also introduced collection factory methods, JShell, and private interface methods:
JDK 10: Local-Variable Type Inference (var)
JDK 11: String Enhancements, HttpClient Standardization
JDK 14: Records (Preview), Pattern Matching for instanceof (Preview)
JDK 15: Text Blocks
2.3 JDK 17: The Maturation of Language Features
JDK 17 finalized all the features that were in preview during JDK 14-16, forming a complete "Modern Java" feature set.
Sealed Classes (JEP 409)
Sealed classes restrict the range of subclasses for a class. When used with pattern matching, the compiler can perform exhaustiveness checks:
The diagram shows the inheritance constraints of Sealed Classes—a sealed interface only allows specified subclasses to implement it, a final subclass cannot be extended further, and a non-sealed subclass opens up extension.
Pattern Matching for instanceof (JEP 394)
Other notable changes in JDK 17:
- Removed the AOT compiler and the Graal interface for the JIT compiler (JEP 410)—meaning if you want to use GraalVM, you need to install it separately.
- Strongly encapsulated JDK internal APIs (JEP 403)—
--illegal-access=permitno longer works. - Removed RMI Activation (JEP 407).
- Restored always-strict floating-point semantics (JEP 306).
2.4 The Transition of JDK 18-20
These three non-LTS versions laid the groundwork for two killer features in JDK 21:
- JDK 18: Default character encoding changed to UTF-8 (JEP 400), Simple Web Server (JEP 408).
- JDK 19: Virtual Threads first preview (JEP 425), Structured Concurrency incubation (JEP 428).
- JDK 20: Scoped Values incubation (JEP 429), Record Patterns second preview (JEP 432).
2.5 JDK 21: A Revolution in the Concurrency Model
JDK 21 is the most significant change to the Java platform since JDK 8. The official release of Virtual Threads fundamentally changes Java's concurrent programming model.
Virtual Threads (JEP 444)
A Virtual Thread cannot simply be described as a "lightweight thread." Its essence is turning threads from an operating system resource into a JVM-managed resource.
Problems with traditional Platform Threads:
- Each thread occupies about 1MB of stack memory (configurable, default
-Xss1m). - Thread creation and destruction involve system calls (
pthread_create/pthread_exit). - High context-switching cost (kernel-mode switching).
- A JVM process can typically create only thousands to tens of thousands of threads.
Implementation mechanism of Virtual Threads:
- Managed by the JVM, not directly mapped to OS threads.
- Uses Continuations to save and restore execution state.
- Automatically unmounts from the carrier thread when encountering a blocking operation (I/O, sleep, locks, etc.).
- After the block is lifted, it is scheduled onto any available carrier thread to continue execution.
- Stack space grows on demand, starting at only a few hundred bytes.