Intermediate

What's New in Java 24

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.

JDK 24 release date: March 18, 2025


Table of Contents

  1. Training overview
  2. Introducing Java 24
  1. Stream Gatherers
  1. Performance & Security Improvements
  1. Conclusion and next steps
  2. Java 24 Change Summary

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:

ModuleThemeDuration
1Introducing Java 2417m 24s
2Stream Gatherers16m 2s
3Performance & Security Improvements14m 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, including String itself
  • More efficient than StringReader: the implementation returned by Reader.of() is not thread-safe, which avoids any synchronization overhead
  • Warning: if a thread-safe implementation is needed, StringReader remains 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:

PrincipleDescription
Complete representationAble to represent all class files
Tree structureReflects the inherent structure of class files: class → fields/methods → code
ImmutabilityAll elements of the model are immutable objects, freely shareable between threads
TransformationCreate new immutable objects from existing objects
BuildersBuilders allow you to build immutable elements
Streaming and materialized viewLazy parsing for increased efficiency (only parses what is accessed)
Modern APIUses recent features: sealed classes, records, lambdas, pattern matching
Format abstractionHide 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 is public, 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:

  1. The destination Path of the new class file
  2. A ClassDesc with the name of the class (NoTests)
  3. A lambda receiving a classBuilder to 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:

  1. Ask the JDK developers (which would cause an API explosion)
  2. 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 map or filter as 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:

GathererOverlapNumber of windows for [A,B,C,D,E] (size 2)Result
windowFixedNo3[[A,B],[C,D],[E]]
windowSlidingYes4[[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 stream
  • A: 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 elements
  • element: the current element of the stream to process
  • downstream: allows you to push elements of the right result type downstream of the pipeline

Return value:

  • true: continue calling integrate with the next elements
  • false: 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 true does 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 state to 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 synchronized blocks
  • Virtual threads are no longer pinned to platform threads because of synchronized blocks
  • Much more scalable implementation of virtual threads

Impact on existing code:

  • Previous recommendation to replace synchronized with java.util.concurrent.locks no 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 synchronized where 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.Unsafe is 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:

ReleaseAction
Java 23Deprecation of Unsafe methods for direct memory access
Java 24These methods produce runtime warnings
FutureComplete 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:

AppearanceBefore Java 24Java 24+
FashionNon-generational (single heap)Generational by default
Organization of the heapA single regionYoung generation (new objects) + Old generation (long-lived objects)
CollectionWhole heap at onceGenerations collected independently
EfficiencyGoodBest (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:

ConstraintDetail
CPU architectureThe AOT cache must be created on the same CPU and architecture as used in runtime
JDK versionThe same JDK version must be used for all 3 steps
ClasspathThe 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:

ThemeFeatures
DeletionWindows x86 32-bit port removed
DepreciationLinux x86 32-bit port deprecated (JDK 25 planned removal)
New APIProcess.waitFor(Duration)
New APIReader.of(CharSequence)
New major APIClass-File API (finalized)
New major APIStream Gatherers API (finalized)
PerformanceGenerational ZGC by default
PerformanceAOT Class Loading & Linking
SecurityRuntime warnings sun.misc.Unsafe
SecurityPreparing Foreign Function & Memory API / JNI opt-in
SecurityQuantum Resistant Crypto Algorithms (ML-KEM, ML-DSA)
Major correctionVirtual 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

CategoryChangeImpact
DeleteWindows x86 32-bit portMigration required for rare users
DepreciationLinux x86 32-bit portRemoval planned in JDK 25
API — ProcesswaitFor(Duration)More ergonomic, modern Duration type
API — IOReader.of(CharSequence)More efficient, no intermediate String conversion
API — BytecodeClass-File API finalized (java.lang.classfile)JDK independent of ASM, beneficiary ecosystem
API — StreamsStream Gatherers finalized (Gatherer<T,A,R>)Scalability of intermediate operations
API — Streams5 built-in gatherers: fold, scan, mapConcurrent, windowFixed, windowSlidingAdvanced operations out of the box
CompetitionVirtual Threads without pinning on synchronizedNo need to migrate to ReentrantLock anymore, mainstream adoption
Securitysun.misc.Unsafe: runtime warningsMigrating to VarHandles / Foreign Function & Memory API
SecurityForeign Function & Memory API / JNI opt-in preparationEnhanced security by default
SecurityML-KEM and ML-DSA (post-quantum crypto)Native FIPS compliance in the JDK
PerformanceGenerational ZGC by defaultReduction of pause times without code change
PerformanceAOT 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.