Tasse de café bleue avec fumée rouge fumante représentant le logo Java 24

Java 24: increasingly high-performance and secure programming

Java 24 marks the next step in the evolution of the language and the JVM, with gradual integration of the Leyden and Lilliput projects. Optimised performance, reduced memory usage, faster start-up: these advances pave the way for a smoother, more efficient experience. While no major syntax revolution is on the agenda, several preview features continue to mature, while security is strengthened by the arrival of post-quantum encryption algorithms. We take a look at the new features that will shape the future of Java.

Each evolution, known as JEP in the Java world, can have one of the following modes:

  • Incubator: incubator modules are a way of putting non-definitive APIs and tools into the hands of developers. These features may evolve considerably, or even disappear between versions.
  • Experimental: these features are currently being tested and are disabled by default (they may be discontinued or completely redesigned).
  • Preview: these fully-specified and implemented features are in the test phase, and can be enabled by users to gather feedback. They are therefore subject to change and are not recommended for production use.
  • Standard: standard features are ready for production use.

#01

Java 24: gradual integration of the Leyden and Liliput projects

Alongside OpenJDK, a number of projects are targeting specific objectives. Following the emergence of JEPs from the Loom and Valhalla projects in recent years, Java 24 is gradually integrating the Leyden and Lilliput initiatives.

The Leyden and Liliput projects

The Leyden project aims to optimise the start-up time of Java applications, reduce their memory footprint and improve their overall performance. For its part, Lilliput tackles management of Java object headers to reduce CPU and memory usage across all workloads.

The Valhalla and Loom projects

The Valhalla project has set itself the ambitious goal of radically rethinking Java’s object model. Java 23 already provided a glimpse of this with the introduction of value classes and value objects. Other advances concern the evolution of primitive and generic types.

As for Loom, it made its mark, particularly with the arrival of Virtual Threads in Java 21. Implementing them required a complete overhaul of the internal workings of the JVM.

#02

Java 24: language evolutions

Java 24 doesn’t introduce any major new features, but several JEPs are extended as previews to encourage user feedback.

JEP 488: Primitive Types in Patterns, instance of, and switch (Second Preview)

Initially introduced as a preview in Java 23 (JEP 455), this feature has been extended without modification. It allows primitive types to be used in patterns, instanceofs and switches.

int x = 65;
if (x instanceof char c) {
  System.out.println("c = " + c); // Sortie : "c = A"
}

JEP 492: Flexible Constructor Bodies (Third Preview)

This feature was introduced in Java 22 (JEP 447), allowing instructions to be executed before the call to super(). Java 23 then enhanced it with support for assignments(JEP 482). In Java 24, it has been renewed for a third preview, with no major changes.

public class PositiveBigInteger extends BigInteger {
  private final long max;

  public PositiveBigInteger(long value, long max) {
      if (value <= 0)
          throw new IllegalArgumentException("non-positive value");
      this.max = max;
      super(value);
  }
}

JEP 494: Module Import Declarations (Second Preview)

Introduced as a preview in Java 23 (JEP 476), this feature allows you to import all classes from packages exported by a module via import module M. Java 24 has been extended for a second preview in order to gather further user feedback.

import module java.util;

String[] fruits = new String[] { "apple", "berry", "citrus" };
Map<String, String> m =
    Stream.of(fruits)
          .collect(Collectors.toMap(s -> s.toUpperCase().substring(0,1),
                                    Function.identity()));

JEP 495: Simple Source Files and Instance Main Methods (Fourth Preview)

This is the fourth preview of this feature, whose first steps were introduced in Java 21 (JEP 445). It simplifies the creation of source files and the main method. For example, a simple HelloWorld programme can be simplified, allowing you to grasp the basics of the language without having to deal with more complex concepts such as classes, the static keyword or visibility.

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Can be simplified to:

main() {
  println("Hello, World!");
}

Several changes have been made to the Java language to facilitate this simplification:

  • It is no longer necessary to declare public static for a main class.
  • For a simple file, declaring a class is no longer mandatory.
  • For a simple file, a number of features from the java.lang package are automatically imported, making it unnecessary to specify System.out.println; simply use println.

#03

Java 24: API developments

472: Prepare to Restrict the Use of JNI

Since Java 22 it has been possible to invoke native code via the Java Native Interface (JNI) or the Foreign Function & Memory (FFM) API. However, native code loading is limited in the FFM API, which by default triggers a runtime warning.

To ensure consistency between the JNI and FFM, native code calls via JNI also generate a warning at runtime.

Here are the messages returned when using the JNI or FFM APIs:

WARNING: A restricted method in java.lang.foreign.Linker has been called
WARNING: java.lang.foreign.Linker::downcallHandle has been called by com.example.Main in an unnamed module
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

To disable warning messages for all Java code, use the option --enable-native-access=ALL-UNNAMED :

java --enable-native-access=ALL-UNNAMED ...

484: Class-File API

Introduced as a preview in Java 22 (JEP 457) and revised in Java 23 (JEP 466), this feature enables direct manipulation of Java class files. Let’s take the example where we want to generate the following method in a class file:

void fooBar(boolean z, int x) {
    if (z)
        foo(x);
    else
        bar(x);
}

The following code illustrates how a method can be generated using Builder, highlighting the API’s specific and transparent approach to code generation:

ClassBuilder classBuilder = ...;
classBuilder.withMethod(
    "fooBar",
     MethodTypeDesc.of(CD_void, CD_boolean, CD_int),
     flags,
     methodBuilder -> methodBuilder.withCode(
         codeBuilder -> {
             Label label1 = codeBuilder.newLabel();
             Label label2 = codeBuilder.newLabel();
             codeBuilder.iload(1)
                        .ifeq(label1)
                        .aload(0)
                        .iload(2)
                        .invokevirtual(ClassDesc.of("Foo"), "foo", MethodTypeDesc.of(CD_void, CD_int))
                        .goto_(label2)
                        .labelBinding(label1)
                        .aload(0)
                        .iload(2)
                        .invokevirtual(ClassDesc.of("Foo"), "bar", MethodTypeDesc.of(CD_void, CD_int))
                        .labelBinding(label2);
                        .return_();
         }
     )
);

JEP 485: Stream Gatherers

Initially previewed in Java 22 (JEP 461) and carried over into Java 23 (JEP 473), this feature allows you to create custom intermediate operations for Streams. Note that it was already possible to define custom terminal operations via the Collector interface.

Here’s an example illustrating the operation intermédiaire Gatherers.limit().

Gatherer<String, ?, String> limit() {
  class Counter {
    int counter;
    Counter(int counter) { this.counter = counter; }
  }
  return Gatherer.ofSequential(
      () -> new Counter(0),
      (counter, element, downstream) -> {
        if (counter.counter++ == 3) {
          return false;
        }
        return downstream.push(element);
      }
  );
}

However, it is less efficient to create custom intermediate operations that reproduce the equivalent of an already available operation, as in the limit() example above. In fact, these custom operations do not benefit from JIT (Just-In-Time) optimisations.

Sécurité : chiffrement post-quantiques

In August 2024, the US National Institute of Standards and Technology (NIST) published a set of encryption and signature tools specifically designed to withstand attacks from a quantum computer. These post-quantum algorithm standards are designed to secure a wide range of electronic information, from confidential e-mails to e-commerce transactions. In response to this development, Java 24 has integrated two of these algorithms into its libraries to enhance long-term data security.

496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism

The Federal Information Processing Standard (FIPS) 203 was designed as the main general encryption standard. The standard is based on the CRYSTALS-Kyber algorithm, renamed ML-KEM for Module-Lattice-Based Key-Encapsulation Mechanism.

// Génération de paire de clés par defaut
KeyPairGenerator g = KeyPairGenerator.getInstance("ML-KEM");
KeyPair kp = g.generateKeyPair(); // an ML-KEM-768 key pair

// Initialisation avec l'algorithme ML-KEM-512
KeyPairGenerator g = KeyPairGenerator.getInstance("ML-KEM");
g.initialize(NamedParameterSpec.ML_KEM_512);
KeyPair kp = g.generateKeyPair(); // an ML-KEM-512 key pair

// Instancier directement un KeyPairGenerator ML-KEM-1024
KeyPairGenerator g = KeyPairGenerator.getInstance("ML-KEM-1024");
KeyPair kp = g.generateKeyPair(); // an ML-KEM-1024 key pair

// Encapsuler une clé
KEM ks = KEM.getInstance("ML-KEM");
KEM.Encapsulator enc = ks.newEncapsulator(publicKey);
KEM.Encapsulated encap = enc.encapsulate();
byte[] msg = encap.encapsulation();     // send this to receiver
SecretKey sks = encap.key();

// Désencapsuler une clé
byte[] msg = ...;                       // received from sender
KEM kr = KEM.getInstance("ML-KEM");
KEM.Decapsulator dec = kr.newDecapsulator(privateKey);
SecretKey skr = dec.decapsulate(msg);

497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm

FIPS 204 is designed to be the main standard for protecting electronic signatures. The standard uses the CRYSTALS-Dilithium algorithm, which has been renamed ML-DSA, short for Module-Lattice-Based Digital Signature Algorithm.

// Génération de paire de clés par defaut
KeyPairGenerator g = KeyPairGenerator.getInstance("ML-DSA");
KeyPair kp = g.generateKeyPair(); // an ML-DSA-65 key pair

// Initialisation avec l'algorithme ML-DSA-44
KeyPairGenerator g = KeyPairGenerator.getInstance("ML-DSA");
g.initialize(NamedParameterSpec.ML_DSA_44);
KeyPair kp = g.generateKeyPair(); // an ML-DSA-44 key pair

// Instancier directement un KeyPairGenerator ML-DSA-87
KeyPairGenerator g = KeyPairGenerator.getInstance("ML-DSA-87");
KeyPair kp = g.generateKeyPair(); // an ML-DSA-87 key pair

// Signer un message avec une clé privée
byte[] msg = ...;
Signature ss = Signature.getInstance("ML-DSA");
ss.initSign(privateKey);
ss.update(msg);
byte[] sig = ss.sign();

// Vérifier une signature avec une clé publique
byte[] msg = ...;
byte[] sig = ...;
Signature sv = Signature.getInstance("ML-DSA");
sv.initVerify(publicKey);
sv.update(msg);
boolean verified = sv.verify(sig);

JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe

The aim of this feature is to prepare the ecosystem for the removal of memory access methods via the sun.misc.Unsafe API, which was introduced in 2002. 

A warning is now issued when an application directly or indirectly uses these methods. 

Although these methods have already been deprecated in Java 23, Java 24 goes a step further by actively notifying the user of their use.

WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::setMemory has been called by com.foo.bar.Server (file:/tmp/foobarserver/thing.jar)
WARNING: Please consider reporting this to the maintainers of com.foo.bar.Server
WARNING: sun.misc.Unsafe::setMemory will be removed in a future release

JEP 486: Permanently Disable the Security Manager

Le gestionnaire de sécurité (Security Manager) est rarement utilisé pour sécuriser le code côté serveur, et sa maintenance est coûteuse. Cette fonctionnalité avait déjà été dépréciée dans Java 17. En raison de sa faible adoption et de l'abandon de son support dans la plupart des frameworks et outils, le Security Manager a été définitivement désactivé dans Java 24.

Previews & incubators

Previews and incubators cover features under development, still in the testing phase and not ready for production use. They are released so that users can enable them and provide feedback.

To enable these features, you need to add the --enable-preview option when compiling and running your code.

  • To compile the code:
    javac --release 24 --enable-preview Main.java
    Then run it with:
    java --enable-preview Main
  • To run the source code directly:
    java --enable-preview Main.java
  • When using jshell:
    jshell --enable-preview

JEP 478: Key Derivation Function API (Preview)

JEP 478 introduces an API for key derivation functions (KDF). These cryptographic algorithms enable additional keys to be derived from a secret key and other data.

// Create a KDF object for the specified algorithm
KDF hkdf = KDF.getInstance("HKDF-SHA256");

// Create an ExtractExpand parameter specification
AlgorithmParameterSpec params =
    HKDFParameterSpec.ofExtract()
                     .addIKM(initialKeyMaterial)
                     .addSalt(salt).thenExpand(info, 32);

// Derive a 32-byte AES key
SecretKey key = hkdf.deriveKey("AES", params);

// Additional deriveKey calls can be made with the same KDF object

Future work enabled by this feature:

  • Refactoring TLS 1.3: TLS 1.3 protocol implementations could benefit from this new KDF API to improve key management. However, there are no plans to integrate this API for earlier versions of TLS.
  • Argon2 implementation: A future version of Java could also include implementation of theArgon2 derivation algorithm, renowned for its resistance to brute-force attacks and its effectiveness in securing passwords.

JEP 499: Structured Concurrency (Fourth Preview)

Structured concurrency is a modern approach made possible by virtual threads, enabling tasks to be divided into sub-tasks and run more efficiently in parallel. After an incubation period during Java 19 and 20, structured concurrency was introduced as a preview in Java 21 (via JEP 453).

As in previous versions (Java 22 and 23), this feature is once again available in Java 24 without major modifications in order to gather more user feedback.

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());
    }
}

JEP 487: Scoped Values (Fourth Preview)

Scoped values allow you to pass one or more values to different methods without having to declare them explicitly as parameters, and without passing them on with each successive call. This approach simplifies management of contextual data, such as the logged-in user or a session.

This feature is being previewed for the fourth time in Java 24 in order to gather further feedback from users.

The following example illustrates how an API can define the logged-in user as a scoped value:

public class Api {
  public final static ScopedValue<User> LOGGED_IN_USER = ScopedValue.newInstance();
  . . .
  private void serve(Request request) {
    . . .
    User loggedInUser = authenticateUser(request);
    ScopedValue.where(LOGGED_IN_USER, loggedInUser)
               .run(() -> apiWebService.processRequest(request));
    . . .
  }
}

JEP 489: Vector API (Ninth Incubator)

The aim of this feature is to introduce an API for expressing vector calculations, which compile reliably into optimal instructions at runtime, on supported CPU architectures.

Vector operations offer a high degree of parallelism, enabling multiple operations to be processed in a single CPU cycle, which can result in significant performance gains. For example, for two vectors each containing a sequence of eight integers, a single hardware instruction can add the elements of both vectors, performing eight additions simultaneously instead of just one.

This feature is in the incubation phase, which means that it is still under development and undergoes major evolutions between each version.

#04

Java 24: changes in the internal workings of the JVM

These enhancements are mainly aimed at optimising JVM performance, without introducing any new features or changes in development practices.

Garbage Collector

The Garbage Collector is responsible for memory management in the JVM. Heap Memory is divided into two sections (or generations): nursery (or young space/young generation) and old space (or old generation).

Most items are created in the Eden space and are then gradually moved to Old Generation as they survive Garbage Collector cycles.

In addition, the JVM offers several types of GC (G1, ZGC, Shenandoah, Serial, Parallel, etc.), each with its own specific features and advantages.

JEP 404: Generational Shenandoah (Experimental)

Until now, the Shenandoah Garbage Collector worked with a single memory zone. This feature now makes it possible to operate it according to the generation principle, as explained above.

It is experimental, meaning that it is currently being tested, and must be enabled explicitly via the options: -XX:+UnlockExperimentalVMOptions -XX:ShenandoahGCMode=generational.

JEP 475: Late Barrier Expansion for G1

The growing popularity of cloud-based Java deployments has put the emphasis on reducing the overall JVM load.

JIT compilation is a key technique for speeding up Java applications, but it entails a significant overload in terms of processing time and memory usage.

The main aim of this feature is to reduce the run time of the C2 optimiser, particularly when used with the G1 garbage collector.

This enhancement optimises performance while minimising the impact on system resources.

JEP 490: ZGC: Remove the Non-Generational Mode

This feature removes the non-generational mode from the Z Garbage Collector (ZGC), keeping the generational mode as default. The main objective is to reduce the maintenance costs associated with managing two separate modes.

Runtime

The main focus of work on the JVM runtime is on improving performance and optimising memory use.

JEP 450: Compact Object Headers (Experimental)

The aim of this feature is to reduce the size of Java object headers. Currently, every Java object contains information such as its class, lock, hashcode, etc., stored in a 96- to 128-bit header. This experiment aims to reduce the size of these headers to 64 bits in order to optimise the JVM’s memory footprint.

It is experimental, which means that it is still in the testing phase. To enable it, the following options must be used when running the JVM: -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders.

JEP 483: Ahead-of-Time Class Loading & Linking

The aim of this feature is to improve start-up time by ensuring that an application’s classes are instantly available, loaded and linked as soon as the Java HotSpot virtual machine is started. This reduces Java application launch times by performing these operations before rather than during runtime.

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

public class HelloStream {

    public static void main(String ... args) {
        var words = List.of("hello", "fuzzy", "world");
        var greeting = words.stream()
            .filter(w -> !w.contains("z"))
            .collect(Collectors.joining(", "));
        System.out.println(greeting);  // hello, world
    }

}

For example, here’s a simple programme using the Stream API. Despite its brevity, it requires you to read, analyse, load and link nearly 600 JDK classes.

This programme runs in 0.031 seconds on JDK 23. However, after creating the AOT cache, it should run in 0.018 seconds on JDK 24, offering a 42% performance improvement.

JEP 491: Synchronize Virtual Threads without Pinning

The aim of this feature is to:

  • enable existing Java libraries to adapt correctly to virtual threads without requiring major modifications, notably by avoiding the use of synchronised methods and instructions;
  • improve diagnostics to identify situations where virtual threads fail to free physical threads (pinning), thus ensuring optimal resource management.

Autres évolutions

JEP 493: Linking Run-Time Images without JMODs

The aim of this feature is to reduce the size of the JDK by around 25% by using the jlink tool to create custom runtime images without using the JDK’s JMOD files. 

This feature must be enabled when the JDK is created, it will not be enabled by default, and some JDK vendors may choose not to enable it.

JEP 479: Remove the Windows 32-bit x86 Port

This marks the removal of source code and support for the Windows 32-bit x86 port, a port deprecated since Java 21.

JEP 501: Deprecate the 32-bit x86 Port for Removal

The 32-bit x86 port, especially for Linux, is now considered obsolete and will be discontinued in a future version. This marks the end of JDK support for this architecture.

Java 24 is the last version before the arrival of Java 25, the next LTS (Long Term Support) version. This is a transitional release, aimed at finalising the features that will be integrated into the LTS version.

This release marks a stabilisation of the language and APIs available in the JDK. Clearly, the focus is now on optimising JVM performance and reducing memory footprint.

Time will tell whether these developments will bear fruit and continue to improve the Java ecosystem.

Otmane Cheddour - Fullstack Design and Development Engineer 

Philippe Bousquet - Java Community Manager, France

Want to know more?