The Holiday Season is three weeks away, but Java developers can already delight in a cool gift from the OpenJDK team, which is a defined set of 24 features in the upcoming JDK 24 release!
That’s right, JDK 24 entered a Rampdown Phase One today, and 24 JEPs are targeted for this release, including eight new features! And the best part? Two of these new features start the introduction of the much anticipated OpenJDK Projects into the Java SE Platform: Project Leyden and Project Lilliput!
Dive into this article to uncover all 24 new, finalized, enhanced, and removed features in JDK 24!
Table of Contents
- New features
- 404: Generational Shenandoah (Experimental)
- 450: Compact Object Headers (Experimental)
- 475: Late Barrier Expansion for G1
- 478: Key Derivation Function API (Preview)
- 483: Ahead-of-Time Class Loading & Linking
- 493: Linking Run-Time Images without JMODs
- 496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism
- 497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm
- Finalized features
- Enhanced features
- 487: Scoped Values (Fourth Preview)
- 488: Primitive Types in Patterns, instanceof, and switch (Second Preview)
- 489: Vector API (Ninth Incubator)
- 491: Synchronize Virtual Threads without Pinning
- 492: Flexible Constructor Bodies (Third Preview)
- 494: Module Import Declarations (Second Preview)
- 495: Simple Source Files and Instance Main Methods (Fourth Preview)
- 499: Structured Concurrency (Fourth Preview)
- Removed and deprecated features
- Preparation for restrictions and removals
- Conclusion
New features
404: Generational Shenandoah (Experimental)
JEP 404 introduced a generational mode to Shenandoah GC.
Shenandoah GC is a low-latency collector that performs most of its work concurrently with the application, including the concurrent compaction, so the pause times will be low regardless of the heap size. But it used to be non-generational meaning that it didn’t divide the objects into young and old generations, like G1 GC, Parallel GC, and as of JDK 23, Z GC. So, developers had to allocate more heap headroom to non-generational Shenandoah. Also, the collector had to mark long-lived objects more frequently and perform more work when collecting the objects.
Generational Shenandoah GC will be able to maintain young and old generations to collect young objects more frequently. It will collect either young or both young and old objects, like G1 GC. But its outstanding feature is that it will perform collections concurrently with the application threads.
In addition, as it will be able to dynamically adapt its generation sizes and related parameters, it will preserve low pause times, but use less memory, and become more performant generally.
Generational mode for Shenandoah GC is currently experimental and has to be explicitly enabled:
-XX:+UnlockExperimentalVMOptions -XX:ShenandoahGCMode=generational
The goal is to make Shenandoah GC generational by default in future releases.
450: Compact Object Headers (Experimental)
JEP 450 marks the introduction of Project Lilliput to the mainline OpenJDK. The Project is aimed at reducing the size of Java object headers on 64-bit architectures.
All objects on the Java heap have headers. An object header can contain a lot of useful information: object pointers, hash code, class, etc. The header consists of a mark word that has multiple purposes (locking, GC data, etc.) and a class pointer that points to the Klass instance. The size of the header is 128 bits on 64-bit platforms regardless of the object size.
This feature will merge together the mark word and the class pointer and compact the resulting header to 64 bits. Compacting the size of object headers will reduce the size of the live data, thus potentially reducing the CPU and/or memory footprint of Java programs.
This feature will be experimental in JDK 24 and can be enabled with
-XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders
The plan is to enable it by default in future releases.
475: Late Barrier Expansion for G1
JEP 475 introduces late barrier expansion for the G1 Garbage Collector. This feature will help to improve the performance of Java applications in the cloud as it will help to reduce the CPU time and memory overhead during the JVM warmup.
To understand the importance of this feature it is necessary to understand what a GC barrier is and how it affects the JVM overhead of JIT compilers.
A JIT compiler compiles Java bytecode into machine code using the sea of nodes concept, which is a type of a dependence graph. The compiler first walks the graph from one node to another, expands each node, and performs code transformations, which include optimizations.
GC barriers contain information about application memory access. A JIT compiler expands GC barrier nodes just like any other node, at an early compilation stage. But this results in significant compilation overhead. In addition, optimizations can break the barriers, resulting in complex issues.
The idea behind late G1 GC barrier extension is to make the JIT compiler expand barrier nodes as late as possible, to the stage when machine code must be emitted. This way, the GC-specific memory access instructions are transformed into machine code according to the relevant barrier information, which was tagged at the early compilation stage. The resulting code may be as performant as C2 optimized code. In addition, late barrier expansion is associated with low C2 overhead.
Z GC has been successfully using late barrier expansion since JDK 14. Late barrier extension was one of the factors guaranteeing the stability of this collector. It was decided that G1 GC should be improved in a similar way. So, many techniques developed for Z GC were re-used for G1 GC.
478: Key Derivation Function API (Preview)
Classical public-key based cryptographic algorithms are becoming increasingly vulnerable to hacker attacks, especially when it comes to quantum computing, when attacks are performed using a supercomputer. As such, the Java platform needs to provide the developers with means of implementing Post-Quantum Cryptography algorithms to secure their application in a modern, more efficient way.
This can be done by integrating support for the Hybrid Public Key Encryption mechanism that uses both public (asymmetric) key encryption and symmetric encryption.
The first step towards HPKE in Java was already implemented in Java 21 with the introduction of Key Encapsulation Mechanism API that helps to secure symmetric keys with asymmetric (public key) cryptography. The next step is JEP 478, which introduces a Key Derivation Function API to the Java platform. The key derivation functions will reside in a new javax.crypto.KDF class.
Prior to JDK 24, Java didn’t have an API specifically for using KDFs. With this API, the developers will be able to use more sophisticated password-hashing functions, such as Argon2, without dealing with the existing APIs not designed for this purpose.
In addition, the support for key derivation functions will be beneficial for applications that interact with cryptographic hardware devices.
483: Ahead-of-Time Class Loading & Linking
JEP 483 marks the initial introduction of Project Leyden to the mainline OpenJDK.
The startup and warmup times of Java applications can be an issue, especially in the cloud. The startup takes several seconds, and the warmup may take several minutes. The goal of the Project is to reduce the startup and warmup times of Java applications by selectively shifting some computations and optimizations from the production run to some earlier stages, for instance, to the training runs.
Ahead-of-Time Class Loading & Linking is the first step in this process. The feature builds on Application Class Data Sharing (CDS). AppCDS reads and parses a set of system and application classes and stores this data in a read-only archive that is then accessible to the JVM at startup. Ahead-of-Time Class Loading & Linking goes further and creates an AOT cache that stores class data after reading, parsing, loading, and linking them. This way, JVM will have even less work to do at startup.
Preliminary tests that used a reference Spring Petclinic application showed that AOT cache gives about 40% improvement in startup time.
493: Linking Run-Time Images without JMODs
JEP 493 introduces a new feature that will enable the jlink tool to cut the size of the JDK by about 25% by excluding the JMOD files from the run-time JDK images.
JMOD files are used by jlink to create a custom Java runtime. With the new option --enable-linkable-runtime
enabled at JDK build time, jlink from the resulting JDK will be able to build a custom runtime without the JMOD files. In addition, it is still possible to build a custom runtime with only modules required for the application to run. The absence of JMOD files will benefit such runtimes even more.
This feature will not be enabled by default, and it remains at the discretion of JDK vendors to enable it.
By the way, Liberica JDK Lite, a flavor of Liberica JDK optimized for the cloud, hasn't included the JMODs files all along. So, you don’t have to wait for JDK 24 and hope that your vendor enables this feature, and instead, use Liberica Runtime Container as an OpenJDK image now!
496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism
JDK 21 included a new security feature, the Key Encapsulation Mechanism (KEM) API. Building on the momentum of equipping the Java platform with quantum-resistant cryptographic algorithms, JEP 496 introduces an implementation of the quantum-resistant Module-Lattice-Based Key-Encapsulation Mechanism (ML-KEM).
ML-KEM has been standardized by the United States National Institute of Standards and Technology (NIST) in FIPS 203. The US government computer systems dealing with sensitive information must be upgraded to use ML-KEM. Therefore, the implementation of this mechanism in the Java platform will facilitate the upgrades of Java applications.
497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm
Continuing on the topic of quantum-resistant security mechanisms in Java, JEP 497 introduces an implementation of the quantum-resistant Module-Lattice-Based Digital Signature Algorithm (ML-DSA).
ML-DSA has been standardized by NIST in FIPS 204. This algorithm is used for signing data and authenticating identities in a quantum-resistant fashion. Just like in the case of ML-KEM, the US government computer systems dealing with sensitive information must be upgraded to use ML-DSA. The implementation of this algorithm in the Java platform will facilitate the upgrades of Java applications.
Finalized features
484: Class-File API
Class files are files containing Java bytecode that can be executed by JVM. They interconnect all parts of the Java ecosystem. Frameworks and other tools such as agents can load or manipulate bytecode to achieve greater performance or gather some data. But they need to understand the bytecode and be able to work with it.
There are numerous libraries for generating, parsing, and transforming class files, and frameworks also bundle a class-file library. But the problem is that the class-file format can evolve faster than framework developers update their class-file library, which can lead to parsing errors.
JEP 484 finalizes a feature that was introduced in JDK 22 as a Preview, namely, Class-File API. This API will evolve together with the class-file format, so that frameworks and tools won’t have to rely on third-party class-file libraries, but on the standard JDK API, meaning that they automatically use the latest class-file format. It means that developers will be able to update the JDK version for their project without risking in most cases that their current framework or tooling versions will be incompatible with the JDK version.
485: Stream Gatherers
JEP 485 finalizes Stream Gatherers, an addition to Stream API that enables the developers to create custom intermediate operations when using Stream API. This feature was introduced as a Preview feature in JDK 22.
Steam API offers a wide but limited range of intermediate operations for processing the data. Sometimes, developers need to perform some custom operations with data. For instance, there’s a distinct()
intermediate operation that helps to filter unique elements, but there is no distinctBy()
operation to filter elements by some other criteria.
Steam Gatherers are aimed at solving this issue. By using a new intermediate operation Stream::gather(Gatherer)
, developers can apply custom logic in a gatherer, which is an instance of a Gatherer interface. It is possible to create your own gatherers by implementing the Gatherer interface or use built-in gatherers from the Gatherers class.
Follow this guide on using Stream Gatherers for defining custom data processing operations.
Enhanced features
487: Scoped Values (Fourth Preview)
JEP 487 introduces a fourth preview of scoped values. Scoped values represent an alternative to thread-local variables and help to write more reliable multi-threaded code.
Traditionally, developers used thread-local variables to share data between methods on a call stack without using method parameters. But thread-local variables are mutable, have unbounded lifetime, and can be associated with significant memory footprint.
Scoped values have a limited lifetime and share immutable data in one way, from caller to callees. Scoped means that the value is accessible only to certain parts of the program, namely the methods invoked directly or indirectly by the run method. This enables a more reliable and maintainable data sharing process in concurrent code.
Scoped values can substitute thread-local variables in many scenarios where one-way transmission of data is implemented. They also bring benefits in terms of memory and time savings when used together with virtual threads and structured concurrency.
One major change in this release is the removal of the callWhere
and runWhere
methods. The only way to use scoped values is via ScopedValue.Carrier.call
and ScopedValue.Carrier.run
methods.
488: Primitive Types in Patterns, instanceof, and switch (Second Preview)
JEP 488 introduces a second preview of primitive types in patterns, instanceof, and switch without changes.
Traditionally, patterns, instanceof
, and switch
haven’t accepted primitive types. It was associated with silent data loss, boilerplate code, and other restrictions.
The ability to use primitive types in all pattern contexts, switch expressions and statements, and instanceof will enable uniform data exploration and eliminate the risk of data loss because of unsafe casts.
For instance, a following switch
statement
switch (x.getStatus()) {
case 0 -> "okay";
case 1 -> "warning";
case 2 -> "error";
default -> "unknown status: " + x.getStatus();
}
Can be improved like this:
switch (x.getStatus()) {
case 0 -> "okay";
case 1 -> "warning";
case 2 -> "error";
case int i -> "unknown status: " + i;
}
In addition, as the instanceof
type test operator now accepts primitive types, it can perform safeguard casting of primitives: if the left-hand value can be safely converted to the right-hand type without loss of information, instanceof returns true. In cases when no checks are needed regardless of the values (for instance, when we want to cast byte to int), the test always returns true. For other cases, a runtime check is necessary:
byte b = 42;
b instanceof int; // true
int i = 42;
i instanceof byte; // true
int i = 1000;
i instanceof byte; // false
489: Vector API (Ninth Incubator)
JEP 489 introduces a ninth incubator of Vector API without changes. This API enables the developers to write vector algorithms that reliably compile at runtime to optimal vector instructions on supported CPU architectures for better performance of scalar operations than with auto-vectorization.
Vector API will be incubated until necessary features of Project Valhalla will be integrated as preview. After that, it will be promoted to a preview feature.
491: Synchronize Virtual Threads without Pinning
JEP 491 introduces an important enhancement to virtual threads.
Virtual threads can mount and unmount platform threads without blocking them, so multiple virtual threads can share one OS thread. However, virtual threads are pinned to the platform threads in synchronized
methods, meaning that they cannot unmount the platform thread.
It may result in a situation when no more virtual threads can be allocated because all platform threads are taken by the virtual threads or blocked in the JVM, which, in turn, impacts scalability.
In JDK 24, the implementation of the synchronized
keyword will be changed in such a way that will allow the virtual threads to mount and unmount the platform thread in the synchronized
method. This will enable the developers to continue using synchronized
methods and at the same time, will eliminate the negative impacts on scalability.
In addition, a jdk.VirtualThreadPinned
event of the Java Flight Recorder, which was issued in cases when a virtual thread was blocked inside a synchronized
method, will be issued in other situations when the virtual thread is pinned, which will help to better diagnose such issues.
492: Flexible Constructor Bodies (Third Preview)
JEP 492 introduces a third preview of flexible constructor bodies without significant changes.
Flexible constructor bodies was introduced in JDK 22 as JEP 447: Statements before super(...). And that’s what this feature is fundamentally about: it allows the developers to place statements in a constructor before an explicit invocation of another constructor with super(..)
or this(..)
.
In this case, it will be possible to avoid writing additional methods and constructors to validate or compute some values necessary for the constructor of the superclass. For example, this code
public class PositiveBigInteger extends BigInteger {
private static long verifyPositive(long value) {
if (value <= 0) throw new IllegalArgumentException(..);
return value;
}
public PositiveBigInteger(long value) {
super(verifyPositive(value));
}
}
Can be expressed in a more laconic way:
public class PositiveBigInteger extends BigInteger {
public PositiveBigInteger(long value) {
if (value <= 0) throw new IllegalArgumentException(..);
super(value);
}
}
As the statements cannot reference the object under construction, this feature makes code more concise and maintainable without breaking the rules for safe object initialization.
494: Module Import Declarations (Second Preview)
JEP 494 introduces a second preview of module import declarations that enable the developers to implicitly import all package modules by simply importing a module:
import module java.base
Such an approach simplifies the import of modular libraries, reduces the noise of multiple on-demand package imports, and facilitates coding in Java for beginners.
This release brings several changes to the feature:
- Allow for the import of the whole Java SE API when importing the java.se module;
- Eliminate ambiguities when several imported modules contain a class with the same name by allowing on-demand import declarations to shadow module import declarations like that:
import module java.base; // exports java.util with a public Date class
import module java.sql; // exports java.sql with a public Date class
import java.sql.Date; // resolve the ambiguity of the name Date
Date d = ... // Date is resolved to java.sql.Date
495: Simple Source Files and Instance Main Methods (Fourth Preview)
JEP 495 introduces a fourth preview of simple source files and instance main methods with slight changes to the terminology and the title, but without any other changes.
This feature enables the developers, who have just started their Java journey, to write simple single-class programs without having to deal with programming-in-the-large, complicated Java concepts that can be encountered even in the simplest possible ‘Hello, World!’ program:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
This feature allows the main methods to not be static and public and have a String[] parameter. Also, it allows you to skip the class declaration. Finally, some methods for the console input and output are automatically imported. As a result, the program above can be simplified to
void main() {
println("Hello, World!");
}
Simple programs can be evolved with time to ordinary Java programs as the knowledge of the beginning developers grows.
499: Structured Concurrency (Fourth Preview)
JEP 499 introduces a fourth preview of structured concurrency without changes.
Structured concurrency is aimed at improving the reliability and observability of the concurrent code.
If the task is broken into subtasks, and each subtask in its own thread, each subtask can succeed or throw an exception, which may lead to two global problems:
- If the developers doesn’t explicitly cancel other subtasks in case of errors, it may lead to thread leaks or interference of tasks with each other;
- Manually handling the lifetime of threads is cumbersome, verbose, and error-prone.
Structured concurrency can help to avoid these issues. The feature is based on the principle that if a task is split into subtasks, they all return to the task’s code block. The task monitors subtasks for failures and waits until they finish.
Response handle() throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> user = scope.fork(() -> findUser());
Supplier<Integer> order = scope.fork(() -> fetchOrder());
scope.join() // Join both subtasks
.throwIfFailed(); // ... and propagate errors
// Here, both subtasks have succeeded, so compose their results
return new Response(user.get(), order.get());
}
}
Entry and exit points of a task’s block are defined clearly, and subtask’s lifetime is confined within the parent code block. This enhances code observability and coordination.
If any of the subtasks fail or a thread running the tasks is interrupted before the results of all subtasks are joined, other subtasks get canceled. This increases code reliability.
Removed and deprecated features
479: Remove the Windows 32-bit x86 Port
The Windows 32-bit x86 Port was deprecated for removal in JDK 21. As the last Windows OS supporting 32-bit operation (Windows 10) will reach end of life in October 2025, there’s no point in supporting this port in OpenJDK.
JEP 479 removes the Windows 32-bit x86 Port completely: all code paths applicable to Windows 32-bit x86 only will be removed, and all testing and development for this platform will be stopped. This will help the OpenJDK community to focus on evolutionizing the Java platform by improving and integrating other, more relevant features.
486: Permanently Disable the Security Manager
The Security Manager API has been around since the first Java version. It was designed for protecting the applications by means of the principle of least privilege: code is untrusted by default, and developers have to explicitly grant specific code permission to access some resources.
In practice, the procedure of giving permissions is so complex that very few applications actually use Security Manager, and it has even been disabled by default. But Java libraries have to implement the principle of least privilege in case the API is enabled, which requires a lot of time and effort. So, it would be better to get read of mainly unused Security Manager than dedicate resources to maintaining compatibility with this API.
As a result, the Security Manager API was deprecated in JDK 17, and JVM was made to issue warnings in case Security Manager was enabled. This gave the developers time to shift away from using Security Manager in case they relied on it.
And the moment has come: JEP 486 permanently disables the Security Manager API.
The API will be removed completely in the future releases.
490: ZGC: Remove the Non-Generational Mode
Z GC, a low latency garbage collector with pause times not longer than 10 ms, became generational in Java 21. Maintaining young and old generations enables the GC to collect young objects more frequently, thus reducing the GC overhead and heap memory overhead.
Generational mode for Z GC became default in JDK 23, and JEP 490 removes the non-generational mode as it is no longer needed.
501: Deprecate the 32-bit x86 Port for Removal
JEP 501 deprecates the 32-bit Linux x86 port, which is the only 32-bit x86 port remaining in the OpenJDK, with the intent of removing it in further releases. As no new hardware is released exclusively for 32-bit x86, and porting new Java features to this architecture requires a lot of work, it will be more beneficial for the OpenJDK contributors to stop maintaining this port and channel their efforts to other, more relevant features and OpenJDK projects.
Preparation for restrictions and removals
472: Prepare to Restrict the Use of JNI
JEP 472 will make JVM issue warnings on the usage of Java Native Interface (JNI). It will also make JVM issue warnings instead of throwing exceptions when Foreign Function & Memory API (FFM API) is used to align the restrictions for JNI and FFM API.
JNI allows Java programs to interact with native code. But these interactions pose risks, including the risk of unexpected JVM crashes, unpredictable GC behavior, and Java code integrity violation. FFM API that was introduced as an alternative to JNI, represents a more reliable approach to interacting with native code, but some risks are applicable to it as well.
Restriction of native access is part of an effort of ensuring that the Java platform has integrity by default. The feature lays ground for further restrictions of native access. In the future, the JVM will throw exceptions instead of issuing warnings both for JNI and FFM API. This will make the Java platform more secure and performant.
But there’s no goal of prohibiting interaction with native code. Java developers will still be able to load and link native libraries using JNI or FFM API, but they will have to explicitly enable native access at application startup. The goal is to let application developers enable native access, not library developers.
498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe
The sun.misc.Unsafe
class could help to increase the performance of Java applications as it contained methods for low-level operations. However, using these methods without due safety checks could lead to the opposite effect: deteriorated performance and unexpected application behavior. As many libraries used methods of sun.misc.Unsafe
without these checks, two more reliable and secure APIs were introduced, Variable Handles and Foreign Function & Memory API.
In turn, the memory-access methods in sun.misc.Unsafe
were deprecated in JDK 23. JEP 498 will make JVM issue warnings if such methods are used.
This is all done with the purpose of preparing the platform for removal of the memory-access methods in sun.misc.Unsafe
in future releases.
Conclusion
Java is getting more powerful with every release, and we are looking forward to the general availability of JDK 24 due in March 2025.
Meanwhile, you can subscribe to our blog to get a monthly digest of fresh articles and videos on all things Java!