JDK 24 has its feature set frozen. This means it’s time to look at what is going to be in the release. In total 24 JEPs are included (Coincidence? I don’t think so!). Let’s skip any experimental, preview and incubating JEPs and review only the generally available ones.
JEP 472: Prepare to Restrict the Use of JNI
In JDK 24 loading a native library in JNI causes a warning unless the module that is loading the library is explicitly approved with --enable-native-access=M1,M2,...
command-line option. Another command-line option --illegal-native-access
is introduced to control how to react to attempts to load native libraries from unapproved modules. By default, it’s set to warn
. allow
(ignores such events) and deny
(throws IllegalCallerException
) are two other options. Eventually deny
will become a new default.
JEP 475: Late Barrier Expansion for G1
A write (read) barrier is a piece of code that gets executed whenever there is an attempt to write to (read from) heap. G1 doesn’t require read barriers, but the write barriers are quite lengthy. Before the JIT compiler compiles a method all memory writes must be augmented with a barrier which can result in the compiler spending a significant portion of time converting the barrier’s code into machine instructions. Instead of compiling the barrier’s code together with the rest of the method’s code this JEP moves the barrier expansion to the post-compilation phase and just pastes the needed machine instructions to the relevant places. As the result the compilation overhead is reduced by 10-20%.
JEP 479: Remove the Windows 32-bit x86 Port
This JEP removes the support of Windows 32-bit x86.
JEP 483: Ahead-of-Time Class Loading & Linking
A big chunk of a Java application startup time is spent on reading, parsing and linking class files. This JEP tries to optimize this by allowing to pre-create a cache file that contains loaded and liked state of classes. Using the file the JVM is able to bypass the stage of parsing and linking classes and therefore start faster. Let’s test it out!
// App.java
import picocli.CommandLine;
import picocli.CommandLine.Command;
@Command(name = "App")
public class App implements Runnable {
@Override
public void run() {
System.out.println("Hello from Command Line!");
}
public static void main(String[] args) {
CommandLine.run(new App(), args);
}
}
$ javac -cp picocli-4.7.6.jar App.java
$ jar -cf myapp.jar App.class
$ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf \
-cp picocli-4.7.6.jar:myapp.jar App
Hello from Command Line!
$ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot \
-cp picocli-4.7.6.jar:myapp.jar App
AOTCache creation is complete: app.aot
$ measure() {
t1=$(date +%s%3N)
$@
t2=$(date +%s%3N)
echo "$((t2-t1)) ms"
}
# Running without the AOT cache
$ measure java -cp picocli-4.7.6.jar:myapp.jar App
Hello from Command Line!
120 ms
# Running with the AOT cache
$ measure java -XX:AOTCache=app.aot -cp picocli-4.7.6.jar:myapp.jar App
Hello from Command Line!
90 ms
As you can see on this synthetic test it gives 25% boost. On more complex applications the effect is going to be even more noticeable.
JEP 484: Class-File API
This JEP finalizes and makes it generally available the Class-File API. The API allows parsing, mutating and generating class files. Although other 3-rd party libraries exist that serve the same purpose it’s challenging for them to keep up with the pace at which the class file format is evolving. It’s going to be beneficial for everyone to have a standard way of dealing with class files as well as for the JVM as it won’t need to rely on a 3-rd party library for that anymore. Let’s look at a simple example of using the API.
// Summator.java
public class Summator {
public static int sum(int a, int b) {
return a + b;
}
}
// Main.java
import java.lang.classfile.*;
import java.nio.file.Path;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
ClassModel cm = ClassFile.of().parse(Path.of("Summator.class"));
System.out.println(cm);
MethodModel mm = cm.methods().stream()
.filter(m -> m.methodName().equalsString("sum")).findFirst().get();
System.out.println(mm);
for (CodeElement ce : cm.methods().get(1).code().get()) {
System.out.println(ce);
}
}
}
java Main.java
ClassModel[thisClass=Summator, flags=33]
MethodModel[methodName=sum, methodType=(II)I, flags=9]
LineNumber[line=3]
Load[OP=ILOAD_0, slot=0]
Load[OP=ILOAD_1, slot=1]
UnboundOperatorInstruction[op=IADD]
Return[OP=IRETURN]
The JEP is an interesting read partially because it goes into some of the API design decisions which might be useful to learn even if you’re not intending to use the API itself.
JEP 485: Stream Gatherers
Since it’s release the Stream API had a limited number of intermediate operations like filter
and map
. Adding more didn’t make much sense as it wouldn’t satisfy every use case. A better idea is to allow the Stream API to accept custom intermediate operations. This is exactly what this JEP is doing.
This change introduces a new method Stream::gather(Gatherer)
which accepts an instance of a new interface called java.util.stream.Gatherer
. In order to create a custom intermediate operation (a gatherer) you’d need to write a class that implements the aforementioned interface. A gatherer can be stateless or stateful, it can produce many output elements given one input element and vice versa, it can support parallel streams.
Let’s look at an example where given a stream of integers the gatherer converts it to a monotonically increasing sequence by dropping those elements that are smaller or equal to the elements already seen.
import java.util.stream.Gatherer;
import java.util.List;
import java.util.function.Supplier;
public class Main {
public static void main(String[] args) {
System.out.println(List.of(1, 2, 3, 1, 2, 3, 4, 5).stream()
.gather(new Monotonize()).toList());
// this prints [1, 2, 3, 4, 5]
}
public static class Monotonize
implements Gatherer<Integer, Monotonize.State, Integer> {
static class State {
Integer maxSeen;
}
@Override
public Supplier<State> initializer() {
return () -> new State();
}
@Override
public Gatherer.Integrator<State, Integer, Integer> integrator() {
return (state, element, downstream) -> {
if (state.maxSeen == null || element > state.maxSeen) {
state.maxSeen = element;
downstream.push(element);
}
return true;
};
}
}
}
The JEP also introduces five built-in gatherers that can be constructed by calling static methods on java.util.stream.Gatherers
:
fold
- reduces the stream down to one element,mapConcurrent
- executes a one-to-one map function concurrently for each stream element using virtual threads given the desired level of concurrency,scan
- given the previous output element and the current stream element allows calculating the next output element,windowFixed
- converts a stream of elements to a stream of fixed-size lists representing non-overlapping windows, for example, given the window size of 3 it converts[1 2 3 4 5 6 7 8]
to[[1 2 3] [4 5 6] [7 8]]
,windowSliding
- converts a stream of elements to a stream of fixed-size lists representing overlapping windows, for example, given the window size of 3 it converts[1 2 3 4 5]
to[[1 2 3] [2 3 4] [3 4 5]]
.
JEP 486: Permanently Disable the Security Manager
Deprecated in Java 17 the Security Manager is now disabled permanently. It was available in Java since the first release with the intention to allow enforcing custom security policies. For example with the Security Manager it was possible to control which files the executed code can access, which network connections it can open and so on. The adoption of the Security Manager was marginal. At the same time it’s a huge burden to maintain it and all places where permission checks against it are required.
As of this release an attempt to enable the Security Manager will result in an error. It won’t allow installing a Security Manager at run time. The Security Manager API is kept but changed to be non-functional.
JEP 490: ZGC: Remove the Non-Generational Mode
The Non-Generational mode of ZGC is removed. The command-line options -XX:+ZGenerational
and -XX:-ZGenerational
no longer make any effect.
JEP 491: Synchronize Virtual Threads without Pinning
Since it’s release in Java 21 Virtual Threads had one major problem: pinning in synchronized methods and blocks. What it means is that whenever a virtual thread entered a synchronized method or block it was pinned to the carrier thread. This prevented the scheduler from being able to release the carrier thread and making it available for other virtual threads that were ready to be run. A carrier thread can be released if the assigned virtual thread stops waiting for an i/o, sleep or any other blocking operation. This problem significantly limited scalability of Virtual Threads. This JEP fixes the problem and allows carrier threads to be released when virtual threads are blocked inside synchronized methods and blocks.
Let’s test it with the following Java program:
import java.io.*;
public class Main {
private static final int NUM_THREADS = 10_000;
public static void main(String []args) throws InterruptedException {
Thread[] vts = new Thread[NUM_THREADS];
long start = System.nanoTime();
for (int i = 0; i < NUM_THREADS; i++) {
vts[i] = Thread.startVirtualThread(() -> {
Object o = new Object();
synchronized (o) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
for (int i = 0; i < NUM_THREADS; i++) {
vts[i].join();
}
long finish = System.nanoTime();
System.out.println(((finish - start) / 1E6) + " ms");
}
}
On Java 21 this code runs for almost 2 minutes while on Java 24 it takes only 250 milliseconds.
JEP 493: Linking Run-Time Images without JMODs
Previously jlink
required JMOD files in order to create a custom run-time image. This JEP makes it possible for jlink
to extract required modules from the run-time image. This allows to not to ship JMOD files which reduces the size of JDK by 25%. This feature is not available by default and must be enabled at the JDK compilation time with --enable-linkable-runtime
configuration flag.
JEP 496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism
A Key Encapsulation Mechanism (KEM) is a way for one party to generate a secret key and securely exchange it over an insecure connection with another party using their public key. This JEP introduces a quantum-resistant KEM implementation to Java called Module-Lattice-Based KEM (ML-KEM). Let’s see in a short example how it works:
import java.security.KeyPairGenerator;
import java.security.spec.NamedParameterSpec;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import javax.crypto.KEM;
import javax.crypto.SecretKey;
import javax.crypto.DecapsulateException;
import java.util.Base64;
public class Main {
public static void main(String[] args) throws NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException,
DecapsulateException {
// generate a key pair
KeyPairGenerator gen = KeyPairGenerator.getInstance("ML-KEM");
gen.initialize(NamedParameterSpec.ML_KEM_1024);
KeyPair kp = gen.generateKeyPair();
// encapsulate a secret key
KEM kemSender = KEM.getInstance("ML-KEM");
KEM.Encapsulator encapsulator = kemSender.newEncapsulator(kp.getPublic());
KEM.Encapsulated encapsulated = encapsulator.encapsulate();
byte[] messageToSend = encapsulated.encapsulation();
SecretKey secretKeyGenerated = encapsulated.key();
System.out.println("Generated and sent: "
+ Base64.getEncoder()
.encodeToString(secretKeyGenerated.getEncoded()));
// send messageToSend to the receiver
byte[] messageReceived = messageToSend.clone();
// decapsulate the secret key
KEM kemReceiver = KEM.getInstance("ML-KEM");
KEM.Decapsulator decapsulator = kemReceiver.newDecapsulator(kp.getPrivate());
SecretKey secretKeyReceived = decapsulator.decapsulate(messageReceived);
System.out.println("Received: "
+ Base64.getEncoder()
.encodeToString(secretKeyReceived.getEncoded()));
}
}
This prints:
Generated and sent: WLWFstGKy3iseKG5+Ze8iXNqjG/u9xu14ff5/Lcp3ZQ=
Received: WLWFstGKy3iseKG5+Ze8iXNqjG/u9xu14ff5/Lcp3ZQ=
JEP 497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm
Another JEP that adds a quantum-resistant algorithm but this time it’s a digital signature algorithm. It allows generating a ML-DSA
key pair, sign a message using its private key and verify the signature using its public key:
import java.security.KeyPairGenerator;
import java.security.spec.NamedParameterSpec;
import java.security.KeyPair;
import java.security.Signature;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.SignatureException;
public class Main {
public static void main(String[] args) throws NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException,
SignatureException {
// generate a key pair
KeyPairGenerator gen = KeyPairGenerator.getInstance("ML-DSA");
gen.initialize(NamedParameterSpec.ML_DSA_87);
KeyPair kp = gen.generateKeyPair();
// sign a message
byte[] message = "A message to be signed.".getBytes();
Signature signer = Signature.getInstance("ML-DSA");
signer.initSign(kp.getPrivate());
signer.update(message);
byte[] signature = signer.sign();
// verify the signature
Signature verifier = Signature.getInstance("ML-DSA");
verifier.initVerify(kp.getPublic());
verifier.update(message);
boolean verified = verifier.verify(signature);
System.out.println("Verification successful: " + verified);
}
}
Verification successful: true
JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe
sun.misc.Unsafe
has never ment to be a public API. Nevertheless, it gained wide adoption among library and application developers who wanted more power over the run-time which the standard APIs couldn’t provide. With the introduction of VarHandle API and Foreign Function & Memory API the existing usages of sun.misc.Unsafe
must be migrated away from.
As of this release any first usage of memory-access methods in sun.misc.Unsafe
is going to result in a warning. In future releases it’s planned to throw exceptions in such cases encouraging maintainers to migrate away from sun.misc.Unsafe
.
JEP 501: Deprecate the 32-bit x86 Port for Removal
This JEP deprecates the 32-bit x86 JDK port. It might be removed in one of the subsequent releases.
Conclusion
We’ve reviewed all generally available features introduced in Java 24. “JEP 485: Stream Gatherers” and “JEP 491: Synchronize Virtual Threads without Pinning” stand out for me the most. It’s nice to see Java continues to evolve. Looking forward for Java 24 to be released.