JDK 24 release date: March 18, 2025
Table of Contents
- 2.1 Deprecations and deletions
- 2.2 New APIs
- 2.3 Introduction to Class-File API
- 2.4 Class-File API examples
- 4.1 Virtual Threads sans pinning sur synchronized
- 4.2 Security Improvements
- 4.3 Performance Improvements
1. Training overview
Java celebrates its 30th anniversary
Java 24 marks a historic milestone: the Java platform reaches 30 years of existence. Despite its age, Java continues to remain relevant and modernize with each release. This training explores the final new features (non-experimental, non-preview) delivered with JDK 24.
Training content
The training is structured into three modules:
| Module | Theme | Duration |
|---|---|---|
| 1 | Introducing Java 24 | 17m 24s |
| 2 | Stream Gatherers | 16m 2s |
| 3 | Performance & Security Improvements | 14m 27s |
- Module 1: deprecations and deletions, API changes, introduction of the new Class-File API
- Module 2: the Stream Gatherers API — a major extension of the existing Streams API (assumes prior knowledge of Java streams)
- Module 3: performance and security improvements — garbage collection, advance-of-time class loading, and more
Important note: This training does not cover experimental features or preview APIs. Java 24 has a lot of them, but they will be covered in future training when they become final.
JDK 24 Download
- Available at jdk.java.net
- Official release date: March 18, 2025
- JDK 24 is not an LTS (Long-Term Support) version
- Java 25 (expected September 2025) will be the next LTS version
- Previous LTS was Java 21
- Teams adopting all releases can use Java 24 features immediately in production
2. Introducing Java 24
2.1 Depreciations and deletions
A new Java release doesn’t just mean new features: it also means deprecations and removals. These changes are positive because they reduce the maintenance burden on JDK developers, freeing up more room for innovation.
Removing Windows x86 32-bit JDK port
The most significant removal of Java 24 is the Windows x86 32-bit port of the JDK.
Context:
- Java is a multi-platform project: Windows, macOS, Linux, x64 architectures, ARM, etc.
- Intel x86 32-bit architecture was dominant in the 90s and early 2000s
- It has now become almost non-existent in modern production environments
- Windows 10 reaches end of life in 2025 — last Windows OS supporting 32-bit x86
Consequences for users:
- Features like virtual threads (introduced in Java 21) already had a sub-optimal implementation on this port, and would not improve over time
- Previous versions of the JDK (notably Java 21 with its extended support) retain their 32-bit x86 builds for those who still need them in production
What is not deleted:
- ARM 32-bit architecture remains supported by the JDK
Linux x86 32-bit port deprecation (for removal in JDK 25)
In the same vein, JDK 24 marks the deprecation for removal of the Linux x86 32-bit port.
Reasons:
- Linux distribution Debian announced the abandonment of 32-bit x86 support
- Situation on Linux is similar to Windows
- Explicit intent is to remove this port in JDK 25 (expected September 2025)
- Increasing maintenance burden no longer justified
- These old architectures hinder the development of major new JDK features, such as the Foreign Function and Memory API, designed for better integration between Java and native code
- In practice, 32-bit x86 hardware is extremely rare and less and less available
Migration:
- If your team still depends on these JDK ports, it is imperative to start migrating to a more modern platform
Other depreciations
Other deprecations related to security and performance will be covered in module 3.
2.2 New APIs
Java 24 introduces two major additions to the API: the Stream Gatherers API (covered in module 2) and the Class-File API (covered in the next section). Before discussing these major APIs, here are two more modest but very useful additions to the core of the Java API.
Addition 1: New overload of Process.waitFor(Duration)
Before Java 24:
To wait for the termination of a child process with a timeout, you had to use:
process.waitFor(30, TimeUnit.SECONDS);
The TimeUnit type comes from the java.util.concurrent package and is used to indicate the unit of the long value (seconds, milliseconds, etc.).
With Java 24:
A new overload accepts a single Duration object from Java’s Date/Time API:
process.waitFor(Duration.ofSeconds(30));
This approach is more intuitive and uses the more modern and familiar Duration type from the java.time API.
Addition 2: Reader.of(CharSequence) — New static method on Reader
Problem resolved:
The StringReader class has been around for a long time to read content from a String. But if you have another implementation of CharSequence — like StringBuffer or StringBuilder — you previously had to convert it to String before creating a reader:
StringBuilder sb = new StringBuilder("hello");
// Avant : coûteux car crée une nouvelle String
Reader r = new StringReader(sb.toString());
This conversion is an expensive operation that allocates an unnecessary intermediate object.
With Java 24:
StringBuilder sb = new StringBuilder("hello");
// Nouveau : pas de conversion intermédiaire
Reader r = Reader.of(sb);
Features of the new implementation:
- Optimized for many popular subtypes of
CharSequence, includingStringitself - More efficient than
StringReader: the implementation returned byReader.of()is not thread-safe, which avoids any synchronization overhead - Warning: if a thread-safe implementation is needed,
StringReaderremains the appropriate choice
2.3 Introduction to the Class-File API
What is the Class-File API?
The Class-File API allows you to parse, generate and transform class files, that is, to work with and manipulate compiled Java bytecode (the .class files).
This API lives in the java.lang.classfile package.
Why does this API exist?
This API is mainly intended for JDK itself and frameworks requiring bytecode manipulation or generation.
Common use cases:
- Frameworks like Hibernate and Spring rely heavily on generating and transforming class files
- The JVM itself generates bytecode on the fly to implement lambdas
- Many JDK tools need to parse class files to work
History:
- Until recently, the JDK included a copy of the ASM library to do all this bytecode manipulation
- Using an existing library is usually the best approach, which explains why things have evolved this way
The chicken and egg problem with ASM
With each Java release (every 6 months), the class file version and sometimes the class file format changes. This creates a circular dependency:
JDK 24 en développement
↓
ASM (en Java 23) ne peut pas dépendre de JDK 24 (pas encore sorti)
↓
JDK 24 ne peut pas utiliser la version ASM supportant son format de class file (ASM pas encore mis à jour)
Additionally, having a strong dependency of the JDK on a third-party library is undesirable.
Solution:
- The Class-File API was introduced in JDK 23 (package
java.lang.classfile) - It is finalized in JDK 24
- It can now evolve in parallel with the JDK, supporting new class file formats with each release
Objectives of the Class-File API
Main goal: Provide enough functionality for the JDK to implement its own use cases, without aiming to completely replace libraries like ASM or Byte Buddy.
Eventually, the hope is that other libraries in the Java ecosystem can adopt this API to always have an up-to-date way of working with the latest class file formats.
Class-File API Design Principles
The Class-File API was designed according to several key principles:
| Principle | Description |
|---|---|
| Complete representation | Able to represent all class files |
| Tree structure | Reflects the inherent structure of class files: class → fields/methods → code |
| Immutability | All elements of the model are immutable objects, freely shareable between threads |
| Transformation | Create new immutable objects from existing objects |
| Builders | Builders allow you to build immutable elements |
| Streaming and materialized view | Lazy parsing for increased efficiency (only parses what is accessed) |
| Modern API | Uses recent features: sealed classes, records, lambdas, pattern matching |
| Format abstraction | Hide class file format details as much as possible |
This approach is much more elegant than the ASM library which relies heavily on the visitor pattern.
2.4 Examples of the Class-File API
Here are concrete examples illustrating the use of the Class-File API.
Example 1: Read a ClassFile into a ClassModel
// 1. Obtenir une instance de ClassFile (avec options par défaut)
ClassFile cf = ClassFile.of();
// 2. Parser le fichier .class depuis le système de fichiers
Path path = Path.of("MonApplication.class");
ClassModel model = cf.parse(path);
// 3. Obtenir un stream de tous les éléments du class file
model.elementStream().forEach(System.out::println);
Typical output:
AccessFlags[flags=PUBLIC]
ClassFileVersion[major=68, minor=0] // 68 = JDK 24
Superclass[name=java/lang/Object]
FieldModel[name=myField]
MethodModel[name=testMethod]
MethodModel[name=regularMethod]
Elements that may be encountered:
AccessFlags: encodes whether the class ispublic,abstract, an interface, an annotation, etc.ClassFileVersion 68: indicates that the class was compiled with JDK 24- Metadata about superclass and implemented interfaces
- Entries for fields (
FieldModel) and methods (MethodModel)
Example 2: Pattern Matching with the Class-File API
The Class-File API is designed to be used with the new language features, including pattern matching:
ClassFile cf = ClassFile.of();
ClassModel model = cf.parse(path);
// Utilisation de pattern matching avec switch
List<String> elements = model.elementStream()
.map(element -> switch (element) {
case MethodModel mm -> "Méthode: " + mm.methodName().stringValue();
case FieldModel fm -> "Champ: " + fm.fieldName().stringValue();
case ClassFileElement cfe -> cfe.toString();
})
.collect(Collectors.toList());
If you have ever used or seen the ASM library via the visitor pattern, you can appreciate how much more elegant this approach is.
Example 3: Transform a ClassFile — Filter Methods
The following example reads a class file containing two methods (regularMethod and testMethod), and writes a new class file that filters all methods whose name begins with test:
ClassFile cf = ClassFile.of();
ClassModel model = cf.parse(sourcePath);
// Construire un nouveau class file filtré
cf.buildTo(outputPath, ClassDesc.of("NoTests"), classBuilder -> {
for (ClassElement element : model) {
// Ignorer les méthodes qui commencent par "test"
if (element instanceof MethodModel mm &&
mm.methodName().stringValue().startsWith("test")) {
continue; // Sauter cette méthode
}
// Inclure tous les autres éléments tels quels
classBuilder.with(element);
}
});
buildTo takes three parameters:
- The destination
Pathof the new class file - A
ClassDescwith the name of the class (NoTests) - A lambda receiving a
classBuilderto build the class
Result: the written class file is identical to the original, except that it does not contain any methods whose name begins with test.
Long-term impact
Although you probably won’t use this API directly, the existence of a standardized API in the JDK will benefit the Java ecosystem in the long run:
- JDK can get rid of its dependency on ASM
- Libraries like Spring and Hibernate, which rely heavily on bytecode manipulation and code generation, will be able to adopt this API over time to always work with the latest class file formats
3. Stream Gatherers
3.1 Why Stream Gatherers?
Reminder: Java’s Streams API
Java 8 introduced the Java Streams API, which allows streaming pipelines to be expressed declaratively to transform elements, as opposed to manually iterating on collections.
List<String> result = List.of("alice", "bob", "charlie", "dave", "eve")
.stream()
.filter(s -> s.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
// Résultat : ["ALICE", "CHARLIE", "DAVE"]
- Intermediate operations:
map,filter,limit,distinct,sorted, etc. (~15 total) - Terminal operation: materializes the stream — the best known being
collect
The richness of the Collector API
The Collector API provides considerable extensibility for terminal operations:
// Joindre en une seule String
stream.collect(Collectors.joining(", "));
// Grouper par une fonction clé
stream.collect(Collectors.groupingBy(String::length));
// Calculer une moyenne
stream.collect(Collectors.averagingInt(String::length));
// Créer son propre Collector personnalisé
stream.collect(myCustomCollector);
It is even possible to implement your own Collector for very specific use cases. In summary, for terminal operations: maximum flexibility and scalability.
The problem of intermediate operations
The ~15 intermediate operations available cover many use cases, but what should you do if you need an intermediate operation that does not yet exist? For example :
- Pair adjacent elements in the stream, to transform them into a stream of pairs
- Add all previous values to the current value before sending it downstream
- Window elements in overlapping groups
These scenarios are either difficult or impossible to express with existing intermediate operators.
Before Java 24, the only options were:
- Ask the JDK developers (which would cause an API explosion)
- Working with complex and inefficient workarounds
The solution: Stream Gatherers (finalized in Java 24)
The Stream Gatherers API provides exactly the same extensibility for intermediate operations that the Collector API provides for terminal operations.
A StreamGatherer is an intermediate operation that can freely:
- Consume items from stream
- Transform, combine, filter or eliminate elements
- Emit items downstream according to any logic
The new gather method on Stream:
Stream<R> gather(Gatherer<? super T, ?, R> gatherer)
A correctly defined Gatherer is compatible with all the characteristics of Java streams:
- Lazy behavior (lazy evaluation)
- Support for infinite streams
- Support for parallel streams (when required)
- Short-circuiting (early termination if necessary)
Note: Technically, we could reimplement
maporfilteras a gatherer. However, this should not be done — gatherers are reserved for use cases that cannot be expressed with existing operations or their combinations.
3.2 Built-in Gatherers
Before creating your own Gatherer, let’s explore the 5 predefined gatherers provided with Java 24. They are found in the Gatherers class, in the java.util.stream package.
1. Gatherers.fold — Collapse to a single value
fold collapses a stream to a single value by starting from an initial value and combining each element with the accumulated value.
List<String> letters = List.of("a", "b", "c", "d", "e");
List<String> result = letters.stream()
.gather(
Gatherers.fold(
() -> "", // Fournisseur de la valeur initiale
(accumulated, next) -> accumulated + next // Fonction de combinaison
)
)
.toList();
// Résultat : ["abcde"] (un seul élément)
Behavior:
- Starts with the initial value (here:
"") - For each element in the stream, combine the accumulated value with the next element
- Emits final only one element as result
2. Gatherers.scan — Accumulation with intermediate transmission
scan is similar to fold, but emits each intermediate accumulated value as part of the resulting stream.
List<Integer> numbers = List.of(2, 5, 3, 7, 9);
List<Integer> result = numbers.stream()
.gather(
Gatherers.scan(
() -> 0, // Valeur initiale
(accumulated, next) -> accumulated + next // Addition
)
)
.toList();
// Résultat : [2, 7, 10, 17, 26]
// (0+2=2), (2+5=7), (7+3=10), (10+7=17), (17+9=26)
Difference with fold: instead of a single final result, scan produces as many elements as the input stream, each representing the value accumulated at that step.
3. Gatherers.mapConcurrent — Concurrent mapping
mapConcurrent applies a mapping function to elements of the stream, but can do so concurrently using virtual threads in its implementation, up to the specified maximum concurrency level.
stream.gather(
Gatherers.mapConcurrent(
4, // Niveau de concurrence max
this::expensiveOperation
)
)
Useful for expensive mapping operations (I/O, network calls) that benefit from parallel execution.
4. Gatherers.windowFixed — Fixed size windows
windowFixed splits the incoming stream into fixed-size windows, with each window emitted as a List<T> containing exactly windowSize elements (except the last one which may be smaller).
List<String> letters = List.of("A", "B", "C", "D", "E");
List<List<String>> windows = letters.stream()
.gather(Gatherers.windowFixed(2))
.toList();
// Résultat : [[A, B], [C, D], [E]]
// 2 fenêtres complètes + 1 fenêtre partielle
5. Gatherers.windowSliding — Sliding windows
windowSliding creates overlapping windows (sliding window), moving forward one element at a time.
List<String> letters = List.of("A", "B", "C", "D", "E");
List<List<String>> windows = letters.stream()
.gather(Gatherers.windowSliding(2))
.toList();
// Résultat : [[A, B], [B, C], [C, D], [D, E]]
// 4 fenêtres qui se chevauchent
Comparison windowFixed vs windowSliding:
| Gatherer | Overlap | Number of windows for [A,B,C,D,E] (size 2) | Result |
|---|---|---|---|
windowFixed | No | 3 | [[A,B],[C,D],[E]] |
windowSliding | Yes | 4 | [[A,B],[B,C],[C,D],[D,E]] |
3.3 Create your own Gatherer
The Gatherer<T, A, R> interface
public interface Gatherer<T, A, R> {
Integrator<A, T, R> integrator();
// + méthodes optionnelles avec implémentations par défaut
}
Three type parameters:
T: type of elements accepted from the input streamA: type of the internal state of the gatherer (optional — not all gatherers need state)R: type of elements emitted downstream (downstream)
This is a functional interface — it has only one mandatory abstract method: integrator().
The other methods have default implementations, only necessary for advanced use cases (state management, combiners for parallel streams, etc.).
The Integrator<A, T, R> interface
The integrator is the heart of the Gatherer:
public interface Integrator<A, T, R> {
boolean integrate(A state, T element, Downstream<R> downstream);
}
Integrate parameters:
state: the optional state object managed during integration, used to transport information between processing multiple elementselement: the current element of the stream to processdownstream: allows you to push elements of the right result type downstream of the pipeline
Return value:
true: continue callingintegratewith the next elementsfalse: bypass the stream — stop processing after this element
Create a Gatherer with Gatherer.of
We can use the static method Gatherer.of(integrator) rather than implementing the interface directly:
// Gatherer pass-through (laisse tout passer)
Gatherer<String, ?, String> passThroughGatherer = Gatherer.of(
(state, element, downstream) -> downstream.push(element)
// ↑ Retourne un boolean :
// true si downstream accepte encore des éléments
);
Example 1: Gatherer pass-through
List<String> letters = List.of("A", "B", "C", "D", "E");
letters.stream()
.gather(Gatherer.of(
(_, element, downstream) -> downstream.push(element)
// _ = state ignoré (utilise underscore, feature Java moderne)
))
.forEach(System.out::println);
// Sortie : A B C D E (tous les éléments passent)
Example 2: Short circuit with return false
letters.stream()
.gather(Gatherer.of(
(_, element, downstream) -> {
downstream.push(element);
return false; // Arrêter après le premier élément
}
))
.forEach(System.out::println);
// Sortie : A (seulement le premier élément)
Best practice: propagate downstream status
letters.stream()
.gather(Gatherer.of(
(_, element, downstream) -> downstream.push(element)
// downstream.push() retourne un boolean :
// true si downstream accepte encore, false s'il est saturé
// En retournant ce boolean, on propage l'état du downstream
// (meilleure performance — évite de pousser vers un downstream saturé)
))
.forEach(System.out::println);
Note: Always returning
truedoes not compromise the correctness of the result, but pushing elements to a downstream that no longer accepts them means that they will be ignored. Propagating the downstream state is therefore preferable for performance.
Example 3: Gatherer with transformation
// Gatherer qui map les éléments via une fonction
<T, R> Gatherer<T, ?, R> mappingGatherer(Function<T, R> mapper) {
return Gatherer.of(
(_, element, downstream) -> downstream.push(mapper.apply(element))
);
}
// Ceci réimplémente map() — uniquement à titre illustratif
Example 4: Gatherer that duplicates each element
Gatherer<String, ?, String> duplicator = Gatherer.of(
(_, element, downstream) -> {
boolean continueStream = downstream.push(element); // Premier push
if (continueStream) {
continueStream = downstream.push(element); // Deuxième push (duplique)
}
return continueStream;
}
);
letters.stream()
.gather(duplicator)
.limit(2) // limit appliqué sur la sortie du gatherer
.toList();
// Résultat : [A, A] (le gatherer émet A deux fois, puis limit coupe)
Composition and sequence
Gatherers can be combined with all other stream operations:
stream
.gather(myFirstGatherer)
.filter(predicate)
.gather(mySecondGatherer)
.map(transformer)
.collect(Collectors.toList());
Advanced points (not covered in the course)
- State management: use
A stateto carry information between calls (example: windowFixed/windowSliding use state to accumulate the elements of a window) - Combiners for stateful gatherers used in parallel streams
- Sequence of gatherers: create a gatherer composed of several gatherers
Recommended resource: The Stream Gatherers tutorial on dev.java — written by the JDK team — contains many useful tips for going further.
4. Performance & Security Improvements
4.1 Virtual Threads without pinning on synchronized
Reminder: Virtual Threads (introduced in Java 21)
virtual threads offer a lightweight alternative to classic threads (platform threads). Here is the conceptual model:
Virtual Thread 1 ──┐
Virtual Thread 2 ──┤──► Platform Thread A ──► OS Thread ──► CPU
Virtual Thread 3 ──┤──► Platform Thread B ──► OS Thread ──► CPU
Virtual Thread N ──┘ (petite quantité) (OS-managed)
Operation:
- A virtual thread is a lightweight abstraction, purely on the Java side
- Its code runs on a platform thread (new name for classic Java threads, which are abstractions on OS threads)
- When a virtual thread encounters a blocking operation (network call, waiting on a lock), it is transparently unmounted from the platform thread
- It will be raised on a platform thread only when the blocking operation ends
- Meanwhile, other virtual threads can run on the freed platform thread
Advantages:
- Developers write simple blocking code in virtual threads
- This code runs efficiently on a small set of platform threads
- thousands or even millions of virtual threads can coexist
- Efficient because the wait time for I/O is usually much greater than the calculation time
The Pinning problem (Java 21)
The original implementation had a known limitation related to the synchronized keyword:
Problem scenario:
public synchronized void someMethod() {
// Le virtual thread détient maintenant un verrou synchronized
networkCall(); // Opération bloquante longue
// ↑ Ici, le virtual thread DEVRAIT être démonté... mais ne peut pas !
// Il est "pinned" (cloué) au platform thread
}
Technical reason: The JVM tracked synchronized locks at platform threads, not virtual threads. This limitation is known as pinning.
Consequences of pinning:
- Even if the platform thread waits without doing anything, other virtual threads cannot be executed there
- With many frequently pinned virtual threads, the platform thread pool can be quickly exhausted
- In some cases this can lead to deadlock situations
Workarounds before Java 24:
The recommendation was to replace synchronized blocks with alternative lock constructs from java.util.concurrent.locks (like ReentrantLock) for critical paths.
Fix in Java 24
Java 24 permanently fixes this problem:
- The JVM now tracks synchronized locks at the level of virtual threads (and no longer platform threads)
- This allows transparent mounting/unmounting of virtual threads even inside
synchronizedblocks - Virtual threads are no longer pinned to platform threads because of
synchronizedblocks - Much more scalable implementation of virtual threads
Impact on existing code:
- Previous recommendation to replace
synchronizedwithjava.util.concurrent.locksno longer necessary - If you have already made these replacements, there is no reason to undo them
- As of Java 24, it is perfectly acceptable to continue using
synchronizedwhere it makes sense
Remaining cases where pinning can still occur
A virtual thread can still be pinned to the platform thread in rarer cases:
- When the virtual thread executes native code via Java Native Integration (JNI)
These cases are less common.
Importance of this improvement
This fix in Java 24 is a significant step for mainstream adoption of virtual threads in the Java ecosystem, making them more of a seamless replacement for traditional threads.
4.2 Security enhancements
Security is always a compromise between safety and flexibility. In recent years, the JVM has gradually strengthened its security by removing features that pose exploitation risks.
Runtime warnings for sun.misc.Unsafe
Context:
sun.misc.Unsafeis a notorious JDK class offering many “escape hatches” around the Java programming model- Officially, it was never a supported API, but through common usage it has become essential for many frameworks and libraries
- The JDK is gradually replacing its dangerous methods for direct memory access with more secure and supported alternatives
Progress:
| Release | Action |
|---|---|
| Java 23 | Deprecation of Unsafe methods for direct memory access |
| Java 24 | These methods produce runtime warnings |
| Future | Complete removal planned |
Migration required: Libraries and frameworks using these methods should migrate to:
VarHandles— access to memory via variable handles- Foreign Function & Memory API — introduced as a more recently supported alternative
Foreign Function & Memory API and JNI becoming opt-in
Preparing in Java 24:
The JDK team is preparing for the Foreign Function & Memory API and Java Native Integration (JNI) to become opt-in features of the Java platform.
Reasoning:
- Interacting with native code and memory outside the JVM is inherently dangerous
- If you are running Java, you should make a conscious choice to enable these potentially dangerous APIs
- Applications that do not need these features will benefit from stronger security by default
Action required: Check the listed command line flags if your application depends on native memory access or native code execution.
Quantum-resistant cryptography
Context: With the advent of quantum computers, current cryptography standards will potentially be easily broken in the future. This is why new quantum-resistant cryptography is being developed and increasingly mandated.
Two quantum-resistant algorithms added in JDK 24:
| Algorithm | Abbreviation | Use | |----------|--------||-------------| | Module-Lattice-Based Key-Encapsulation-Mechanism | ML-KEM | Protecting symmetric keys using public key cryptography in a quantum-resistant manner | | Module-Lattice-Based Digital Signature Algorithm | ML-DSA | Creating quantum-resistant digital signatures to prevent message tampering |
These two algorithms are defined by the American NIST Institute and the FIPS standards (Federal Information Processing Standards).
Practical impact:
- If your application uses JDK cryptography APIs, review — preferably with your security experts — where and how to replace existing uses with these new quantum-resistant alternatives
- FIPS compliance can now be achieved with the functionality of the JDK itself, without third-party libraries
4.3 Performance improvements
1. Generational ZGC — Garbage Collector Improvement
The Z Garbage Collector (ZGC):
- ZGC is not the default GC of the JVM
- To activate it, add the following flag when starting the application:
java -XX:+UseZGC -jar myapp.jar
Benefits of ZGC (already available before Java 24):
- Drastically reduces GC pause times compared to Parallel GC and G1 GC (the current default GC)
- Benchmarks show significant improvements in pause times
The big change in Java 24 — ZGC Generational by default:
| Appearance | Before Java 24 | Java 24+ |
|---|---|---|
| Fashion | Non-generational (single heap) | Generational by default |
| Organization of the heap | A single region | Young generation (new objects) + Old generation (long-lived objects) |
| Collection | Whole heap at once | Generations collected independently |
| Efficiency | Good | Best (most objects die young — weak generation theory) |
Impact:
- If you already use ZGC: free performance benefit with Java 24
- If you are not using ZGC yet: strongly recommended to try it with Java 24
2. Ahead-of-Time Class Loading & Linking (AOT CL&L)
The problem: Every start of the JVM with your application requires loading classes from the classpath, parsing them, verifying them, linking them, etc. This represents a significant overhead for the startup.
The solution: AOT Class Loading & Linking
This feature aims to reduce startup time by creating an AOT cache containing the already loaded, verified and linked representations of your application classes, which can be instantly available at startup.
3-step process:
Step 1 — Training run:
# Démarrer l'application en mode enregistrement
java -XX:AOTMode=record \
-XX:AOTConfiguration=myapp.aotconf \
-cp myapp.jar MyApp
- The application runs normally but saves an AOT configuration (
myapp.aotconf) - This execution must be representative of production usage — trigger the paths and flows that users will actually trigger
Step 2 — Creating the AOT cache:
# Créer le cache AOT à partir de la configuration
java -XX:AOTMode=create \
-XX:AOTConfiguration=myapp.aotconf \
-XX:AOTCache=myapp.aot \
-cp myapp.jar
- The application is not actually executed in this step
- All class loading, parsing and linking is done ahead of time based on configuration
- Results are written to an AOT cache file (
myapp.aot)
Step 3 — Running with AOT cache:
# Démarrer l'application avec le cache AOT
java -XX:AOTCache=myapp.aot \
-cp myapp.jar MyApp
- All class information is read effectively from cache
- JVM bypasses much of the usual startup work
Results and benefits:
- Preliminary testing shows up to 40% reduction in startup time
- Significant improvement without changing application code
Constraints and limitations:
| Constraint | Detail |
|---|---|
| CPU architecture | The AOT cache must be created on the same CPU and architecture as used in runtime |
| JDK version | The same JDK version must be used for all 3 steps |
| Classpath | The classpath and other options must be consistent between the 3 steps |
Conclusion: This new capability of Java 24 can really boost your application’s startup time with minimal effort.
5. Conclusion and next steps
What we covered in this training
This training explored the new final (non-preview, non-experimental) features of Java 24:
| Theme | Features |
|---|---|
| Deletion | Windows x86 32-bit port removed |
| Depreciation | Linux x86 32-bit port deprecated (JDK 25 planned removal) |
| New API | Process.waitFor(Duration) |
| New API | Reader.of(CharSequence) |
| New major API | Class-File API (finalized) |
| New major API | Stream Gatherers API (finalized) |
| Performance | Generational ZGC by default |
| Performance | AOT Class Loading & Linking |
| Security | Runtime warnings sun.misc.Unsafe |
| Security | Preparing Foreign Function & Memory API / JNI opt-in |
| Security | Quantum Resistant Crypto Algorithms (ML-KEM, ML-DSA) |
| Major correction | Virtual Threads without pinning on synchronized |
What is not covered
Java 24 contains many non-final features (preview, experimental, incubating). They will be covered in future training when they become final.
To learn more
- Complete list of Java 24 JEPs (Java Enhancement Proposals): available at openjdk.org — each JEP details in depth why the feature was added and how it works
- Stream Gatherers tutorial: on dev.java — written by the JDK team, contains many indications to go further
- Other “What’s New in Java” courses: previous courses from Sander Mak on Pluralsight (Java 21, Java 22, Java 23)
6. Summary of Java 24 changes
Version context
Java 21 (LTS) → Java 22 → Java 23 → Java 24 → Java 25 (LTS, sept. 2025)
- JDK 24: release of March 18, 2025
- Not an LTS version
- Includes all improvements from previous versions
Complete summary table
| Category | Change | Impact |
|---|---|---|
| Delete | Windows x86 32-bit port | Migration required for rare users |
| Depreciation | Linux x86 32-bit port | Removal planned in JDK 25 |
| API — Process | waitFor(Duration) | More ergonomic, modern Duration type |
| API — IO | Reader.of(CharSequence) | More efficient, no intermediate String conversion |
| API — Bytecode | Class-File API finalized (java.lang.classfile) | JDK independent of ASM, beneficiary ecosystem |
| API — Streams | Stream Gatherers finalized (Gatherer<T,A,R>) | Scalability of intermediate operations |
| API — Streams | 5 built-in gatherers: fold, scan, mapConcurrent, windowFixed, windowSliding | Advanced operations out of the box |
| Competition | Virtual Threads without pinning on synchronized | No need to migrate to ReentrantLock anymore, mainstream adoption |
| Security | sun.misc.Unsafe: runtime warnings | Migrating to VarHandles / Foreign Function & Memory API |
| Security | Foreign Function & Memory API / JNI opt-in preparation | Enhanced security by default |
| Security | ML-KEM and ML-DSA (post-quantum crypto) | Native FIPS compliance in the JDK |
| Performance | Generational ZGC by default | Reduction of pause times without code change |
| Performance | AOT Class Loading & Linking | -40% potential startup time |
Method for enabling performance features
# ZGC (Z Garbage Collector)
java -XX:+UseZGC -jar myapp.jar
# ZGC + mode générationnel explicite (déjà défaut dans Java 24)
java -XX:+UseZGC -XX:+ZGenerational -jar myapp.jar
# AOT Cache — Étape 1 : enregistrement
java -XX:AOTMode=record -XX:AOTConfiguration=myapp.aotconf -cp myapp.jar MyApp
# AOT Cache — Étape 2 : création du cache
java -XX:AOTMode=create -XX:AOTConfiguration=myapp.aotconf -XX:AOTCache=myapp.aot -cp myapp.jar
# AOT Cache — Étape 3 : utilisation du cache au runtime
java -XX:AOTCache=myapp.aot -cp myapp.jar MyApp
JEPs associated with Java 24 (reference)
For each feature, one or more JEPs (Java Enhancement Proposals) describe in detail the motivation and operation. The full list is available at openjdk.org/projects/jdk/24.
Search Terms
java · backend · architecture · full-stack · web · api · class-file · gatherer · gatherers · covered · jdk · performance · pinning · stream · windows · 32-bit · addition · classfile · collector · depreciations · improvement · improvements · interface · method