Why You Should Consider Migrating to Java 24 ??

Why You Should Consider Migrating to Java 24 ??

This is my first blog post in many, many years, and I was wondering what to write about. I’m a community guy — I love sharing content! I’ve been part of the Java Noroeste JUG leadership for over a decade, and one thing I always enjoy is talking about the latest Java releases.

Back in September 2024, I gave a talk at a SouJava meetup about Java 23 features. So, I thought, why not do the same here but for Java 24? Let’s get straight to the point and talk about all the cool new things coming with Java 24 and why you should be thinking about migrating!

1?? Virtual Threads Just Got Even Better! (JEP 491)

Virtual Threads (VTs) were a game-changer in Java 21, making concurrency easier and more scalable. But there was a problem: if you used synchronized methods, they could pin the carrier thread, blocking other tasks and hurting performance.

?? What changed in Java 24? With JEP 491, synchronized methods no longer block the carrier thread! Now, virtual threads can park safely, making them much more efficient for applications that rely on traditional synchronization mechanisms.

?? Example:

Looking at the example below, pinning usually happens when a virtual thread is blocked inside a synchronized block for an extended period.

import java.util.concurrent.*;

public class VirtualThreadPinningTest {
    private static final Object lock = new Object();

    static void blockingTask() {
        synchronized (lock) { // ?? This used to pin in Java 21
            System.out.println(Thread.currentThread() + " - Holding lock...");
            try {
                Thread.sleep(3000); // Simulate a long blocking operation
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread() + " - Released lock!");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        var executor = Executors.newVirtualThreadPerTaskExecutor();

        long start = System.currentTimeMillis();

        executor.submit(VirtualThreadPinningTest::blockingTask);
        Thread.sleep(500); // Ensures the first thread locks first
        executor.submit(VirtualThreadPinningTest::blockingTask);

        executor.shutdown();
        executor.awaitTermination(5, TimeUnit.SECONDS);

        long elapsed = System.currentTimeMillis() - start;
        System.out.println("Total execution time: " + elapsed + "ms");
    }
}        

Let’s explore how things turn out with Java 21:

$ javac --release 21 VirtualThreadPinningTest.java                                                                                                         

$ java -Djdk.tracePinnedThreads=full VirtualThreadPinningTest                                                                                  
VirtualThread[#20]/runnable@ForkJoinPool-1-worker-1 - Holding lock...
VirtualThread[#20]/runnable@ForkJoinPool-1-worker-1 reason:MONITOR
    java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:199)
    java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393)
    java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:640)
    java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:817)
    java.base/java.lang.Thread.sleepNanos(Thread.java:494)
    java.base/java.lang.Thread.sleep(Thread.java:527)
    VirtualThreadPinningTest.blockingTask(Main.java:11) <== monitors:1
    java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
    java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
    java.base/java.lang.VirtualThread.run(VirtualThread.java:329)
VirtualThread[#20]/runnable@ForkJoinPool-1-worker-1 - Released lock!
VirtualThread[#25]/runnable@ForkJoinPool-1-worker-3 - Holding lock...
Total execution time: 5516ms        

If we do the same in Java 24, the thread is no longer pinned within a synchronized block:

$javac --release 24 Main.java

$ java --enable-preview -Djdk.tracePinnedThreads=full VirtualThreadPinningTest                        
VirtualThread[#26]/runnable@ForkJoinPool-1-worker-1 - Holding lock...
VirtualThread[#26]/runnable@ForkJoinPool-1-worker-1 - Released lock!
VirtualThread[#30]/runnable@ForkJoinPool-1-worker-2 - Holding lock...
Total execution time: 5522ms        

Although this is a simple example, you’ll notice it seems slower. However, the thread is no longer pinned, allowing other threads to run freely. If you’re using virtual threads, no changes are needed — your existing code will now scale more efficiently.

2?? G1 GC Just Got Smarter (JEP 475)

If you’re running Java apps in the cloud, you’re probably using G1 GC. Java 24 introduces a performance optimization called Late Barrier Expansion, which reduces JVM overhead by delaying the insertion of GC barriers until later in the compilation process.

?? What does this mean?

  • Lower GC overhead ?
  • Faster JVM performance ?
  • More efficient cloud deployments ?

It’s a low-level change that makes Java even better for high-performance and cloud-native applications.

3?? Faster Startup with Ahead-of-Time Class Loading (JEP 483)

Have you ever felt your Spring Boot app takes too long to start? Java 24brings Ahead-of-Time (AOT) Class Loading & Linking, which reduces startup time by up to 42% when the JVM is trained properly.

?? Example:

Let' take this small Java program as an example, it the Stream API, which loads almost 600 JDK classes when executed.

import java.util.*;
import java.util.stream.*;

public class HelloStream {
    public static void main(String ... args) {
        long start = System.currentTimeMillis();
        var words = List.of("hello", "fuzzy", "world");
        var greeting = words.stream()
                .filter(w -> !w.contains("z"))
                .collect(Collectors.joining(", "));
        System.out.println(greeting);  // Output: hello, world
        long elapsed = System.currentTimeMillis() - start;
        System.out.println("Total execution time: " + elapsed + "ms");

    }
}        

? First, compile the program:

$ javac HelloStream.java        

? Run it once to observe normal startup time:

$ java HelloStream 
hello, world
Total execution time: 9ms        

? Then run the application in a training mode to let the JVM analyze and record its AOT configuration:

$ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf \                                          
     -cp . HelloStream           
hello, world
Total execution time: 25ms        

This will generate an AOT configuration file (app.aotconf), storing details about the required classes and methods.

? Now, use the recorded configuration to create an AOT cache:

$ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf \                                          
     -XX:AOTCache=app.aot -cp . HelloStream            
AOTCache creation is complete: app.aot        

This does not run the program but instead generates an optimized cache file (app.aot) that speeds up future executions.

? Now, run the program using the cache:

$ java -XX:AOTCache=app.aot -cp . HelloStream
hello, world
Total execution time: 1ms        

This results in faster startup because classes are loaded instantly from the cache rather than being read, parsed, and linked at runtime.

To conclude, the JVM trains itself by learning which classes are used often and preloads them. This helps reduce startup time and improves performance.

?? Great for:

  • Spring Boot ??
  • Micronaut, Quarkus ??
  • Serverless applications ?

Less waiting, more coding!

4?? New Class-File API (JEP 484)

If you’ve ever worked with bytecode manipulation using ASM or other low-level tools, you know how painful it can be. Java 24 introduces a standard Class-File API, making it much easier to read, write, and transform class files without low-level hacks.

?? Example: Using the new Class-File API

This example we will: ? Create a new HelloWorld class ? Generate the main() method that prints text ? Write the .class file that can be loaded and executed by the JVM

import java.lang.classfile.ClassFile;
import java.lang.constant.ClassDesc;
import java.lang.constant.MethodTypeDesc;
import java.nio.file.Path;

public class Main {
    public static void main(String[] args) throws Exception {
        Path classFilePath = Path.of("HelloWorld.class");
        ClassFile.of().buildTo(classFilePath, ClassDesc.of("HelloWorld"), classBuilder -> {
            // Set class metadata
            classBuilder
                    .withVersion(68, 0) // Java 24 = major version 68
                    .withSuperclass(ClassDesc.of("java.lang.Object")); // Superclass name

            // Add the "main" method
            classBuilder.withMethod(
                    "main",
                    MethodTypeDesc.ofDescriptor("([Ljava/lang/String;)V"), // void method with String[] parameter
                    ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC, // public static
                    methodBuilder -> methodBuilder.withCode(code -> code // System.out.println("Hello, world from Java 24!")
                            .getstatic(ClassDesc.of("java.lang.System"), "out", ClassDesc.of("java.io.PrintStream"))
                            .ldc("Hello, world from Java 24!")
                            .invokevirtual(ClassDesc.of("java.io.PrintStream"), "println", MethodTypeDesc.ofDescriptor("(Ljava/lang/String;)V"))
                            .return_())
            );
        });
        System.out.println("Generated HelloWorld.class successfully!");
    }
}        

? Compile with Java 24

javac Main.java        

? Run & Verify the Bytecode

$ java Main                   
Generated HelloWorld.class successfully!

$ java HelloWorld             
Hello, world from Java 24!        

If you’re building instrumentation tools, agents, or compilers, this is a huge win!

5?? Stream.gather() — More Powerful Stream Transformations (JEP 485)

Java Streams just got a new intermediate operation: Stream.gather(). It allows more flexible transformations, including: ? One-to-one ? One-to-many ? Many-to-many

?? Example: Before vs. After

Before Java 24, you had to flatten streams manually:

In this example we want to convert an array like [1, 2, 3, 4, 5, 6, 7] into [[1, 2, 3], [4, 5, 6], [7]].

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

public class Main {
    public static void main(String[] args) {
        // Collect all elements into a list first
        List<Integer> allNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
        int batchSize = 3;

        // Generate batches from the list
        Stream<List<Integer>> batches = Stream.iterate(0, i -> i < allNumbers.size(), i -> i + batchSize)
                .map(i -> allNumbers.subList(i, Math.min(i + batchSize, allNumbers.size())));

        System.out.println(batches.toList());
    }
}        

With Stream.gather(), it’s much simpler and more efficient:

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

public class Main {
    public static void main(String[] args) {
        System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7).gather(Gatherers.windowFixed(3)).toList());
    }
}        

Output for both should be an array of arrays divided in blocks of 3 items:

$ javac Main.java
$ java Main
[[1, 2, 3], [4, 5, 6], [7]]        

This makes working with nested or complex transformations a lot easier!

6?? JDK Linking Without JMODs (JEP 493)

The JDK is getting smaller! With the new --enable-linkable-runtime option, you can build a JDK that allows jlink to create runtime images without JMOD files. This enables it to link JDK modules directly from the containing run-time image, resulting in a run-time image that is about 60% smaller than a full JDK run-time image.

? Same modules ? Smaller footprint ? Faster deployments

If you’re shipping custom JDK run-time image, this is great news!

7?? Quantum-Resistant Cryptography (JEP 496 & 497)

With quantum computing advancing, traditional cryptographic algorithms like RSA and ECC could become vulnerable. Java 24 introduces post-quantum security with:

?? ML-KEM (Kyber) — For key exchange ? ML-DSA (Dilithium) — For digital signatures

This is huge for securing future-proof applications!

?? What Else Is Changing in Java 24?

?? New Features & Enhancements (Preview & Experimental)

Java 24 introduces several experimental and preview features that push the platform forward:

?? Performance & Memory Improvements

  • JEP 404: Generational Shenandoah (Experimental) — Adds a generational mode to the Shenandoah garbage collector for better memory efficiency.
  • JEP 450: Compact Object Headers (Experimental) — Reduces object header size to improve memory footprint.

?? Security & Cryptography

  • JEP 478: Key Derivation Function API (Preview) — Introduces a standardized API for key derivation functions to enhance cryptographic security.

?? Concurrency & Scoped Values

  • JEP 487: Scoped Values (Fourth Preview) — Improves efficiency over thread-local variables by providing immutable, inheritable values within a scope.
  • JEP 499: Structured Concurrency (Fourth Preview) — Simplifies concurrent programming by treating related tasks as a single unit.

??? Language & Syntax Enhancements

  • JEP 488: Primitive Types in Patterns, instanceof, and switch (Second Preview) — Enhances pattern matching to support primitive types.
  • JEP 494: Module Import Declarations (Second Preview) — Simplifies module imports with new syntax.
  • JEP 495: Simple Source Files and Instance Main Methods (Fourth Preview) — Allows instance mainmethods and streamlines single-file execution.

? Performance-Oriented APIs

  • JEP 489: Vector API (Ninth Incubator) — Continues enhancing SIMD vector operations for better CPU utilization.
  • JEP 492: Flexible Constructor Bodies (Third Preview) — Adds more flexibility in constructor execution.

??? Deprecations, Removals & Restrictions

Java 24 also phases out outdated features:

?? JNI & Unsafe Restrictions

  • JEP 472: Prepare to Restrict the Use of JNI — Lays groundwork for restricting Java Native Interface (JNI) in future releases.
  • JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe– Warns developers about unsafe memory operations.

?? Feature & Platform Removals

  • JEP 479: Remove the Windows 32-bit x86 Port — Ends support for 32-bit Windows.
  • JEP 501: Deprecate the 32-bit x86 Port for Removal — Signals the eventual removal of 32-bit x86 support.
  • JEP 486: Permanently Disable the Security Manager — Final removal of Java’s outdated Security Manager.
  • JEP 490: ZGC: Remove the Non-Generational Mode — Fully transitions ZGC to a generational model.

Final Thoughts

Java 24 brings major improvements in performance, scalability, and security. Whether you’re running cloud apps, microservices, or high-performance applications, there’s something valuable here.

So… are you upgrading to Java 24? Let me know! ??

Happy coding! ??


要查看或添加评论,请登录

Thiago G.的更多文章

社区洞察

其他会员也浏览了