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

Java 24 : quelles sont les nouveautés ?

Java 24 marque une nouvelle étape dans l'évolution du langage et de la JVM, avec l’intégration progressive des projets Leyden et Lilliput. Performances optimisées, consommation mémoire réduite, démarrage accéléré : ces avancées ouvrent la voie à une expérience plus fluide et efficiente. Si aucune révolution syntaxique majeure n’est au programme, plusieurs fonctionnalités en preview poursuivent leur maturation, tandis que la sécurité se renforce avec l’arrivée d’algorithmes de chiffrement post-quantiques. 
Décryptage des nouveautés de Java 24 !

Chaque évolution, nommée JEP dans le monde Java, peut posséder l’un des modes suivants :

  • Incubator : les modules d’incubation sont un moyen de mettre des API et des outils non définitifs entre les mains des développeurs. Ces fonctionnalités peuvent fortement évoluer, voire disparaitre entre les versions.
  • Experimental : ces fonctionnalités sont en cours d’expérimentations et sont désactivées par défaut (il se peut que celles-ci soient abandonnées ou totalement repensées).
  • Preview : ces fonctionnalités totalement spécifiées et implémentées sont en phase de test et activables par les utilisateurs afin de susciter leurs retours. Elles peuvent donc évoluer et ne sont pas conseillées pour un usage en production.
  • Standard : les fonctionnalités standard sont prêtes à être utilisées en production.

#01

Java 24 : intégration progressive des projets Leyden et Liliput

En parallèle d’OpenJDK, plusieurs projets ciblent des objectifs spécifiques. Après l’émergence de JEP issues des projets Loom et Valhalla ces dernières années, Java 24 intègre progressivement les initiatives Leyden et Lilliput.

Les projets Leyden et Lilliput

Le projet Leyden vise à optimiser le temps de démarrage des applications Java, à réduire leur empreinte mémoire et à améliorer leurs performances globales. De son côté, Lilliput s’attaque à la gestion des headersdes objets Java afin de diminuer la consommation CPU et mémoire sur l’ensemble des charges de travail.

Les projets Valhalla et Loom

Le projet Valhalla poursuit l’ambitieux objectif de repenser en profondeur le modèle objet de Java. Java 23 en a déjà donné un aperçu avec l’introduction des value classes et value objects. D’autres avancées portent sur l’évolution des types primitifs et des génériques.

Quant à Loom, il a marqué les esprits, notamment avec l’arrivée des Virtual Threads dans Java 21. Leur implémentation a exigé une refonte complète du fonctionnement interne de la JVM.

#02

Java 24 : les évolutions du langage

Java 24 n’introduit pas de nouvelle fonctionnalité majeure, mais plusieurs JEP sont reconduites en preview pour encourager les retours des utilisateurs.

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

Initialement introduite en preview dans Java 23 (JEP 455), cette fonctionnalité est prolongée sans modification. Elle permet l’utilisation des types primitifs dans les patterns, les instanceof et les switch.

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

JEP 492 : Flexible Constructor Bodies (Third Preview)

Cette fonctionnalité a été introduite dans Java 22 (JEP 447), permettant d'exécuter des instructions avant l'appel à super(). Java 23 l'a ensuite enrichie avec la prise en charge des affectations (JEP 482). Dans Java 24, elle est reconduite pour une troisième preview, sans modification majeure.

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)

Introduite en preview dans Java 23 (JEP 476), cette fonctionnalité permet d'importer toutes les classes des packages exportés par un module via import module M. Java 24 la reconduit pour une seconde preview, afin de recueillir davantage de retours utilisateurs.

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)

Il s'agit de la quatrième preview de cette fonctionnalité, dont les premières étapes ont été introduites dans Java 21 (JEP 445). Elle permet de simplifier la création de fichiers sources et de la méthode main. Par exemple, un simple programme HelloWorld peut être simplifié, permettant ainsi d’appréhender les bases du langage sans avoir à aborder des concepts plus complexes tels que les classes, le mot-clé static, ou la visibilité.

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

Peut être simplifiée en :

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

Plusieurs modifications ont été apportées au langage Java pour faciliter cette simplification :

  • Il n'est plus nécessaire de déclarer public static pour une classe main.
  • Pour un fichier simple, la déclaration d'une classe n'est plus obligatoire.
  • Pour un fichier simple, un certain nombre de fonctionnalités du package java.lang sont automatiquement importées, ce qui rend inutile la spécification de System.out.println; il suffit d'utiliser simplement println.

#03

Java 24 : les évolutions concernant les APIs

472 : Prepare to Restrict the Use of JNI

Depuis Java 22 il est possible d'invoquer du code natif via l'interface Java Native Interface (JNI) ou l'API Foreign Function & Memory (FFM). Cependant, le chargement de code natif est limité dans l'API FFM, ce qui déclenche par défaut un avertissement à l'exécution.

Pour garantir une cohérence entre JNI et FFM, l'appel de code natif via JNI génère également un avertissement pendant l'exécution.

Voici les messages retournés lors de l'utilisation des API JNI ou FFM :

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

Pour désactiver les messages d’avertissement pour l’ensemble du code java, il est possible d’utiliser l’option --enable-native-access=ALL-UNNAMED :

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

484 : Class-File API

Cette fonctionnalité, introduite en preview dans Java 22 (JEP 457) et révisée dans Java 23 (JEP 466), permet de manipuler directement les fichiers de classe Java. Prenons l'exemple où l'on souhaite générer la méthode suivante dans un fichier de classe :

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

Le code suivant illustre comment une méthode peut être générée à l’aide de Builder, mettant en évidence l’approche spécifique et transparente de l’API pour la génération de code :

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

Cette fonctionnalité, initialement proposée en preview dans Java 22 (JEP 461) et reprise en Java 23 (JEP 473), permet de créer des opérations intermédiaires personnalisées pour les Streams. À noter qu'il était déjà possible de définir des opérations terminales personnalisées via l'interface Collector.

Voici un exemple illustrant l’opération 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);
      }
  );
}

Cependant, il est moins performant de créer des opérations intermédiaires personnalisées qui reproduisent l’équivalent d’une opération déjà disponible, comme dans l’exemple du limit() ci-dessus. En effet, ces opérations personnalisées ne profitent pas des optimisations JIT (Just-In-Time).

Sécurité : chiffrement post-quantiques

Le National Institute of Standards and Technology (NIST) des États-Unis a publié en août 2024 un ensemble d’outils de chiffrement et de signature spécifiquement conçus pour résister aux attaques d’un ordinateur quantique. Ces normes d’algorithmes post-quantiques visent à sécuriser une large gamme d’informations électroniques, allant des courriels confidentiels aux transactions de commerce électronique. En réponse à cette évolution, Java 24 a intégré deux de ces algorithmes dans ses librairies pour renforcer la sécurité des données à long terme.

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

La Federal Information Processing Standard (FIPS) 203, a été conçue comme la principale norme de chiffrement général. La norme est basée sur l’algorithme CRYSTALS-Kyber, renommé ML-KEM, abréviation de 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

La FIPS 204, est conçue comme la principale norme pour la protection des signatures électroniques. La norme utilise l’algorithme CRYSTALS-Dilithium, qui a été renommé ML-DSA, abréviation de 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

L’objectif de cette fonctionnalité est de préparer l’écosystème à la suppression des méthodes d’accès mémoire via l’API sun.misc.Unsafe, qui a été introduite en 2002. 

Un avertissement est désormais émis lorsqu’une application utilise directement ou indirectement ces méthodes. 

Bien que ces méthodes aient déjà été dépréciées dans Java 23, Java 24 va plus loin en notifiant activement l’utilisateur de leur utilisation,

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.

Les previews & incubators

Les previews et incubators représentent des fonctionnalités en cours de développement, encore en phase de test et non prêtes pour une utilisation en production. Elles sont mises à disposition pour permettre aux utilisateurs de les activer et fournir des retours.

Pour activer ces fonctionnalités, il est nécessaire d'ajouter l'option --enable-preview lors de la compilation et de l'exécution de votre code.

  • Pour compiler le code :
    javac --release 24 --enable-preview Main.java
    Puis l'exécuter avec :
    java --enable-preview Main
  • Pour exécuter directement le code source :
    java --enable-preview Main.java
  • Lors de l'utilisation de jshell :
    jshell --enable-preview

JEP 478 : Key Derivation Function API (Preview)

Le JEP 478 introduit une API pour les fonctions de dérivation de clés (KDF). Ces algorithmes cryptographiques permettent de dériver des clés supplémentaires à partir d'une clé secrète et d'autres données.

// 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

Futur travaux permis par cette fonctionnalité :

  • Refactorisation du TLS 1.3 : l'implémentation du protocole TLS 1.3 pourrait tirer parti de cette nouvelle API KDF pour améliorer la gestion des clés. En revanche, il n'est pas prévu d'intégrer cette API pour les versions antérieures de TLS.
  • Implémentation d’Argon2 : Une future version de Java pourrait également inclure une implémentation de l'algorithme de dérivation d’Argon2, réputé pour sa résistance aux attaques par force brute et son efficacité dans la sécurisation des mots de passe.

JEP 499 : Structured Concurrency (Fourth Preview)

La concurrence structurée est une approche moderne rendue possible par les threads virtuels, permettant de diviser les tâches en sous-tâches et de les exécuter en parallèle de manière plus efficace. Après une période d'incubation pendant Java 19 et 20, la concurrence structurée a été introduite en preview dans Java 21 (via la JEP 453).

Comme dans les versions précédentes (Java 22 et 23), cette fonctionnalité est de nouveau proposée en Java 24 sans modifications majeures, afin de recueillir davantage de retours utilisateurs.

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)

Les Scoped Values permettent de transmettre une ou plusieurs valeurs à différentes méthodes sans devoir les déclarer explicitement en tant que paramètres et sans les transmettre à chaque appel successif. Cette approche simplifie la gestion des données contextuelles, comme l'utilisateur connecté ou une session.

Cette fonctionnalité est proposée pour une quatrième fois en preview dans Java 24, afin de recueillir davantage de retours des utilisateurs.

L'exemple suivant illustre comment une API peut définir l'utilisateur connecté en tant que 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)

L'objectif de cette fonctionnalité est d'introduire une API permettant d'exprimer des calculs vectoriels, qui se compilent de manière fiable en instructions optimales lors de l'exécution, sur les architectures CPU prises en charge.

Les opérations vectorielles offrent un degré de parallélisme élevé, permettant de traiter plusieurs opérations en un seul cycle CPU, ce qui peut entraîner des gains de performance significatifs. Par exemple, pour deux vecteurs contenant chacun une séquence de huit entiers, une seule instruction matérielle peut additionner les éléments des deux vecteurs, réalisant huit additions simultanément au lieu d'une seule.

Cette fonctionnalité est en incubation, ce qui signifie qu'elle est encore en développement et subit des évolutions majeures entre chaque version.

#04

Java 24 : évolutions du fonctionnement interne de la JVM

Ces améliorations visent principalement à optimiser les performances de la JVM, sans introduire de nouvelles fonctionnalités ou de changements dans les pratiques de développement.

Garbage Collector

Le ramasse-miettes (ou Garbage Collector en anglais) est responsable de la gestion de la mémoire dans la JVM. La Heap Memory est divisée en deux sections (ou générations) : pépinière (ou espace jeune/jeune génération) et espace ancien (ou ancienne génération).

La plupart des objets sont créés dans l’espace Eden, ils sont alors déplacés progressivement vers la Old Generation au fur et à mesure qu’ils survivent aux cycles du Garbage Collector.

De plus la JVM propose plusieurs types de GC (G1, ZGC, Shenandoah, Serial, Parallel, etc.) qui ont chacun leurs spécificités et leurs avantages.

JEP 404 : Generational Shenandoah (Experimental)

Jusqu’à présent, le Garbage Collector Shenandoah fonctionnait avec une seule zone mémoire. Cette fonctionnalité permet désormais de le faire fonctionner selon le principe de génération, comme expliqué précédemment.

Elle est expérimentale, c’est à dire qu’elle est en cours de tests, elle doit être activée explicitement via les options : -XX:+UnlockExperimentalVMOptions -XX:ShenandoahGCMode=generational.

JEP 475 : Late Barrier Expansion for G1

La popularité croissante des déploiements Java basés sur le cloud a mis l'accent sur la réduction de la charge globale de la JVM. 

La compilation JIT est une technique clé pour accélérer les applications Java, mais elle entraîne une surcharge importante en termes de temps de traitement et d’utilisation de la mémoire.

L’objectif principal de cette fonctionnalité est de réduire le temps d’exécution de l’optimiseur C2, en particulier lorsqu’il est utilisé avec le collecteur de déchets G1. 

Cette amélioration permet ainsi d’optimiser les performances tout en minimisant l'impact sur les ressources système.

JEP 490 : ZGC: Remove the Non-Generational Mode

Cette fonctionnalité supprime le mode non générationnel du Z Garbage Collector (ZGC), en conservant le mode générationnel comme valeur par défaut. L’objectif principal est de réduire les coûts de maintenance associés à la gestion de deux modes distincts.

Runtime

Les principaux travaux autour du runtime de la JVM se concentrent sur l'amélioration des performances et l'optimisation de l'utilisation de la mémoire.

JEP 450 : Compact Object Headers (Experimental)

L’objectif de cette fonctionnalité est de réduire la taille des en-têtes des objets Java. Actuellement, chaque objet Java contient des informations telles que sa classe, son verrouillage, son hashcode, etc., stockées dans un en-tête de 96 à 128 bits. Cette expérimentation vise à réduire la taille de ces en-têtes à 64 bits, afin d'optimiser l'empreinte mémoire de la JVM.

Elle est expérimentale, ce qui signifie qu’elle est encore en phase de test. Pour l'activer, il est nécessaire d'utiliser les options suivantes lors de l'exécution de la JVM : -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders.

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

L’objectif de cette fonctionnalité est d'améliorer le temps de démarrage en permettant que les classes d'une application soient instantanément disponibles, chargées et liées dès le démarrage de la machine virtuelle Java HotSpot. Cela permet de réduire les délais de lancement des applications Java en effectuant ces opérations avant l'exécution, plutôt que pendant.

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
    }

}

Par exemple, voici un programme simple qui utilise l'API Stream. Malgré sa brièveté, il nécessite de lire, analyser, charger et lier près de 600 classes du JDK.

Ce programme s'exécute en 0,031 seconde sur JDK 23. Cependant, après la création du cache AOT, il devrait s'exécuter en 0,018 seconde sur JDK 24, offrant ainsi une amélioration de 42 % des performances.

JEP 491 : Synchronize Virtual Threads without Pinning

Cette fonctionnalité a pour objectif de :

  • Permettre aux bibliothèques Java existantes de s'adapter correctement aux threads virtuels sans nécessiter de modifications majeures, notamment en évitant l'utilisation des méthodes et instructions synchronized.
  • Améliorer les diagnostics pour identifier les situations où les threads virtuels échouent à libérer les threads physiques (pinning), garantissant ainsi une gestion optimale des ressources.

Autres évolutions

JEP 493 : Linking Run-Time Images without JMODs

L’objectif de cette fonctionnalité est de réduire la taille du JDK d’environ 25 % en utilisant l’outil jlink pour créer des images d’exécution personnalisées sans recourir aux fichiers JMOD du JDK. 

Cette fonctionnalité doit être activée lors de la création du JDK, elle ne sera pas activée par défaut, et certains fournisseurs de JDK pourraient choisir de ne pas l’activer.

JEP 479 : Remove the Windows 32-bit x86 Port

Cette évolution marque la suppression du code source et de la prise en charge du port Windows 32 bits x86, un port déprécié depuis Java 21.

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

Le port x86 32 bits, notamment pour Linux, est désormais considéré comme obsolète et sera abandonné dans une version future. Cela marque la fin du support pour cette architecture dans le JDK.

Java 24 représente la dernière version avant l’arrivée de Java 25, la prochaine version LTS (Long Term Support). Il s’agit d’une release de transition, visant à finaliser les fonctionnalités qui seront intégrées dans la version LTS.

Cette version marque une stabilisation du langage et des APIs proposées dans le JDK. Il est évident que les efforts se concentrent désormais sur l'optimisation des performances de la JVM et la réduction de l'empreinte mémoire.

L’avenir nous dira si ces évolutions porteront leurs fruits et continueront à améliorer l'écosystème Java.

Otmane Cheddour, Ingénieur Concepteur Développeur Fullstack
Philippe Bousquet, Architecte Technique 

Vous souhaitez échanger avec un expert ?