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?
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:
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
?? Security & Cryptography
?? Concurrency & Scoped Values
??? Language & Syntax Enhancements
? Performance-Oriented APIs
??? Deprecations, Removals & Restrictions
Java 24 also phases out outdated features:
?? JNI & Unsafe Restrictions
?? Feature & Platform Removals
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! ??