JDK 24 Key Features — Stream Gatherers Finalized and a Record 24 JEPs

A Record 24 JEPs

JDK 24, released on March 18, 2025, ships with 24 JEPs, the most changes ever included in a single release. With Stream Gatherers finalized, Virtual Thread pinning resolved, and quantum-resistant cryptography introduced, many features have immediate practical impact.

This article covers the most important features with runnable examples.

Stream Gatherers (JEP 485, Final)

Stream Gatherers, which appeared as a preview in JDK 22, are finally finalized in JDK 24. The existing Stream API’s map, filter, and flatMap are specialized for stateless transformations, but stateful operations like window processing and accumulative transformations were hard to express.

The Gatherers utility class provides frequently used patterns out of the box.

import java.util.List;
import java.util.stream.Gatherers;
import java.util.stream.Stream;

public class StreamGatherersDemo {
    public static void main(String[] args) {
        List<Integer> data = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // windowFixed(n): split into non-overlapping blocks of size n
        List<List<Integer>> fixedWindows = data.stream()
            .gather(Gatherers.windowFixed(3))
            .toList();
        System.out.println("Fixed window(3): " + fixedWindows);
        // Output: Fixed window(3): [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]

        // windowSliding(n): sliding window — useful for moving averages
        List<List<Integer>> slidingWindows = data.stream()
            .gather(Gatherers.windowSliding(4))
            .toList();
        System.out.println("Sliding window(4): " + slidingWindows);
        // Output: Sliding window(4): [[1, 2, 3, 4], [2, 3, 4, 5], ..., [7, 8, 9, 10]]

        // scan(): generate cumulative values as a stream (running sum, etc.)
        List<Integer> cumSum = data.stream()
            .gather(Gatherers.scan(() -> 0, Integer::sum))
            .toList();
        System.out.println("Cumulative sum: " + cumSum);
        // Output: Cumulative sum: [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]

        // fold(): return only the final accumulated value (similar to reduce but uses initial value factory)
        List<Integer> total = data.stream()
            .gather(Gatherers.fold(() -> 0, Integer::sum))
            .toList();
        System.out.println("Total sum: " + total);
        // Output: Total sum: [55]

        // Practical example: calculate moving average with sliding window
        List<Double> movingAvg = data.stream()
            .gather(Gatherers.windowSliding(3))
            .map(window -> window.stream()
                .mapToInt(Integer::intValue)
                .average()
                .orElse(0.0))
            .toList();
        System.out.println("3-day moving average: " + movingAvg);
        // Output: 3-day moving average: [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
    }
}

The difference between windowFixed() and windowSliding(): windowFixed divides data into non-overlapping blocks, while windowSliding generates overlapping blocks by shifting one position at a time. windowSliding is ideal for calculating moving averages on time-series data.

Virtual Thread Pinning Fix (JEP 491, Final)

Virtual Threads introduced in JDK 21 had a pinning problem. When blocking I/O was performed inside a synchronized block, the Virtual Thread would be “pinned” to its Carrier Thread, preventing other Virtual Threads from running.

JDK 24’s JEP 491 resolves this at the JVM level. Virtual Threads now properly yield even inside synchronized blocks.

import java.util.concurrent.Executors;
import java.util.concurrent.CountDownLatch;

public class VirtualThreadPinningFixed {
    // Synchronization object protecting shared resources
    private static final Object lock = new Object();
    private static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        int taskCount = 100;
        CountDownLatch latch = new CountDownLatch(taskCount);

        long start = System.currentTimeMillis();

        // Run 100 tasks concurrently with Virtual Threads
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < taskCount; i++) {
                executor.submit(() -> {
                    synchronized (lock) {
                        // Before JDK 24: sleeping in this synchronized block would pin the Carrier Thread
                        // JDK 24: properly yields to other Virtual Threads without pinning
                        try {
                            Thread.sleep(10); // I/O wait simulation
                            counter++;
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                    latch.countDown();
                });
            }
        }

        latch.await();
        long elapsed = System.currentTimeMillis() - start;

        System.out.println("Tasks processed: " + counter);
        System.out.println("Total time: " + elapsed + "ms");
        System.out.println("Pinning: does not occur in JDK 24");
        // Example output:
        // Tasks processed: 100
        // Total time: 1050ms
        // Pinning: does not occur in JDK 24
    }
}

This change is significant because most existing Java libraries use synchronized. Before JDK 24, using such libraries with Virtual Threads could actually degrade performance. From JDK 24 onward, you can fully benefit from Virtual Threads without modifying existing code.

Quantum-Resistant Cryptography (JEP 496/497, Final)

When quantum computers become practical, widely-used encryption algorithms like RSA and ECDSA could be broken. JDK 24 officially supports two quantum-resistant (Post-Quantum) cryptographic algorithms standardized by the US NIST.

JEPAlgorithmPurpose
JEP 496ML-KEM (Module-Lattice Key Encapsulation)Key exchange
JEP 497ML-DSA (Module-Lattice Digital Signature)Digital signatures
import java.security.KeyPairGenerator;
import java.security.KeyPair;
import java.security.Signature;

public class QuantumResistantDemo {
    public static void main(String[] args) throws Exception {
        // ML-DSA: Quantum-resistant digital signature algorithm
        // Based on NIST FIPS 204 standard
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("ML-DSA");
        keyGen.initialize(65); // ML-DSA-65 (security level: equivalent to 128-bit)
        KeyPair keyPair = keyGen.generateKeyPair();

        System.out.println("Algorithm: " + keyPair.getPublic().getAlgorithm());
        System.out.println("Public key size: " + keyPair.getPublic().getEncoded().length + " bytes");
        // Example output:
        // Algorithm: ML-DSA
        // Public key size: 1952 bytes

        // Create signature
        Signature signer = Signature.getInstance("ML-DSA");
        signer.initSign(keyPair.getPrivate());
        byte[] message = "Quantum-resistant signature test".getBytes();
        signer.update(message);
        byte[] signature = signer.sign();
        System.out.println("Signature size: " + signature.length + " bytes");
        // Example output: Signature size: 3309 bytes

        // Verify signature
        Signature verifier = Signature.getInstance("ML-DSA");
        verifier.initVerify(keyPair.getPublic());
        verifier.update(message);
        boolean valid = verifier.verify(signature);
        System.out.println("Signature verification: " + (valid ? "valid" : "invalid"));
        // Output: Signature verification: valid
    }
}

Quantum-resistant cryptography keys and signatures are larger than traditional RSA/ECDSA. This is because the mathematical structure is different — lattice-based problems are believed to be resistant to efficient solving even by quantum computers. To defend against “Harvest Now, Decrypt Later” attacks, transitioning to quantum-resistant cryptography should start now.

Other Notable JEPs

Class-File API (JEP 484, Final): A standard API for reading and writing bytecode. Previously dependent on external libraries like ASM, now .class files can be generated and transformed with a built-in JDK API. A significant change for framework developers.

AOT Class Loading and Linking (JEP 483): Caches class loading and linking results at application startup, reducing startup time on subsequent restarts. Effective at mitigating Cold Start issues in microservices environments.

Compact Object Headers (JEP 450, Experimental): An experimental feature reducing object header size from 12 bytes to 8 bytes. Finalized in JDK 25, it significantly reduces heap usage for memory-intensive applications.

jlink Runtime Image without JMODs (JEP 493): jlink can now generate runtime images without JMOD files, reducing JDK distribution size by about 25%. Contributes to image size optimization in container-based deployments.

ZGC Non-Generational Mode Removal (JEP 490): Following JDK 23 making generational mode the default, JDK 24 completely removes Non-Generational mode. The -XX:-ZGenerational option can no longer be used.

G1 Late Barrier Expansion (JEP 475): Moves G1 GC barrier code generation to a later stage of JIT compilation, increasing optimization opportunities for the JIT compiler.

Practical Tips

FeatureStatusKey Points
Stream GatherersFinalBuilt-in windowFixed, windowSliding, scan, fold
Virtual Thread Pinning FixFinalProper yield even inside synchronized
ML-KEM/ML-DSAFinalNIST standard quantum-resistant key exchange/signatures
Class-File APIFinalBytecode manipulation without ASM
AOT Class LoadingFinalClass loading cache on restart
Compact Object HeadersExperimentalObject header 12B -> 8B
jlink Size ReductionFinalJDK size reduced 25%

If you could pick one thing to try first from JDK 24, it would be Stream Gatherers. If you work with time-series data or need window operations in batch processing, Gatherers.windowFixed() and windowSliding() solve the problem without external libraries. If you’re already using Virtual Threads, upgrading to JDK 24 alone eliminates pinning issues — a free performance improvement.

Was this article helpful?