Java 25 : Quoi de neuf?
Maintenant que Java 25 est features complete (Rampdown Phase One au jour d’écriture de l’article), c’est le moment de faire le tour des fonctionnalités qu’apporte cette nouvelle version, à nous, les développeurs.
Cet article fait partie d’une suite d’article sur les nouveautés des dernières versions de Java, pour ceux qui voudraient les lire en voici les liens : Java 24, Java 23, Java 22, Java 21, Java 20, Java 19, Java 18, Java 17, Java 16, Java 15, Java 14, Java 13, Java 12, Java 11, Java 10, et Java 9.
Java 25 est la nouvelle version à support à long terme (LTS), elle contient pas moins de 18 JEPs. Même si c’est moins bien que les 24 JEPs de Java 24 c’est quand même un chiffre assez important.
Pas de grosses nouveautés au programme, mais plusieurs petites nouveautés sympas tout de même.
JEP 470: PEM Encodings of Cryptographic Objects (Preview)
Nouvelle fonctionnalité en preview qui apporte le support du format Privacy-Enhanced Mail (PEM) à Java.
Ce format est très répandu pour communiquer, par exemple, des clés ou des certificats, car c’est un format textuel simple à utiliser. C’est aussi un standard de PKCS#8.
Ce format est maintenant supporté pour les clés privées, les clés publiques, les certificats et les listes de révocations de certificats.
Un texte PEM est une représentation encodée en Base64 d’un objet cryptographique.
Par exemple, pour une clé publique de type elliptic curve :
-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/kRGOL7wCPTN4KJ2ppeSt5UYB6u cPjjuKDtFTXbguOIFDdZ65O/8HTUqS/sVzRF+dg7H3/tkQ/36KdtuADbwQ== -----END PUBLIC KEY-----
Pour encoder un objet cryptographique au format PEM on va utiliser la nouvelle classe PEMEncoder
:
PEMencoder pe = PEMEncoder.of(); // plaintext byte[] pem = pe.encode(publicKey); // password encrypted byte[] pemWithPassword = pe.withEncryption(password).encode(privateKey);
On peut fournir un mot de passe pour encoder le texte PEM avec la méthode withEncryption()
; on peut aussi directement encoder en String via la méthode encodeToString()
.
Pour décoder un texte PEM en un objet cryptographique, on va utiliser la nouvelle classe PEMDecoder
:
PEMDecoder pd = PEMDecoder.of(); // plaintext Certificate cert = pd.decode(pem, X509Certificate.class); // password encrypted Certificate certWithPassord = pd.withDecryption(password) .decode(pem, X509Certificate.class);
Plus d’information dans la JEP 470.
JEP 502: Stable Values (Preview)
Nouvelle fonctionnalité en preview qui permet de créer des variables stable qui vont se comporter comme une variable finales mais avec une plus grande flexibilité pour leur initialisation.
Une stable value ne peut être initialisée qu’une seule fois et est immuable.
Il est assez classique de retarder l’initialisation d’un attribut d’une classe lors de sa première utilisation pour éviter de ralentir le démarrage de l’application, ou même éviter d’initialiser un attribut non utilisé !
Pour faire ça de manière cohérente et sûre dans des scénarios multi-thread, il faut utiliser une construction assez compliquée à base de double-checked locking ou de class-holder idiom.
Avec les Stable Values, nous avons maintenant une API pour ça !
private final StableValue<Logger> logger = StableValue.of(); Logger getLogger() { return logger.orElseSet(() -> Logger.create(MyClass.class)); } void saySomething() { getLogger().info("I have nothing to say"); }
Il y a plusieurs manières de créer des stable values, au lieu d’utiliser orElseSet()
on peut utiliser un supplier, exemple :
private final Supplier<Logger> logger = StableValue .supplier(() -> Logger.create(MyClass.class)); void saySomething() { logger().get().info("I have nothing to say"); }
La JVM va garantir que la stable value logger
va être initialisé une et une seule fois, lors de sa première utilisation, et va permettre au JIT d’en optimiser l’utilisation (constant folding).
Il est aussi possible de construire une liste de stable values, dont chaque élément va être initialisé une seule fois lors de son premier accès. Très pratique pour créer des pools d’objets.
static final List<Connection> connections = StableValue .list(POOL_SIZE, _ -> new Connection()); void doSomething() { long index = Thread.currentThread().threadId() % POOL_SIZE; Connection con = connections.get((int) index); }
Plus d’information dans la JEP 502.
JEP 503: Remove the 32-bit x86 Port
Le port 32-bit pour les architectures x86 de la JVM a été déprécié en Java 24 avec l’intention de le supprimer dans une release future, c’est désormais chose faite.
Le coût de maintenance et de développement de nouvelles fonctionnalités induit par ce port ne valait pas la peine, car cette architecture n’est quasiment plus utilisée. Cela n’impacte pas les autres architecture 32-bit tels que ARM32.
Pour les architectures non supportées, il existe un port de la JVM sans spécificité d’architecture : le port Zero, qui permettra toujours de faire tourner une JVM sous architecture 32-bit x86.
509: JFR CPU-Time Profiling (Experimental)
Fonctionnalité expérimentale qui ajoute la capture des informations de profiling du temps CPU sous Linux à Java Flight Recorder (JFR).
JFR est l’outil de monitoring de production de la JVM, il a un overhead faible et permet déjà de monitorer l’utilisation mémoire d’une méthode et son temps d’exécution (wall-clock time).
Pour monitorer le temps d’exécution d’une méthode, JFR va sampler la stack d’exécution d’un programme à intervalles réguliers, par exemple toutes les 20ms.
Le temps d’exécution d’une méthode n’est pas forcément le temps CPU, par exemple, une méthode faisant des calculs en mémoire comme un tri de tableau va principalement consommer du CPU donc son temps d’exécution sera quasiment équivalent au temps CPU, au contraire, une méthode accédant à une ressource réseau va principalement attendre des I/O et va donc avoir un temps CPU très faible.
Les systèmes Linux supportent la mesure précise de la consommation des cycles CPU via l’émission de signal à intervalles réguliers. C’est ce qui est utilisé par la plupart des profilers Linux et dans le monde Java, par exemple par async-profiler.
Avec la JEP 509, JFR peut maintenant utiliser le mécanisme offert par Linux pour émettre un nouvel événement de monitoring: jdk.CPUTimeSample
, ce nouvel événement n’est pas actif par défaut.
La ligne de commande suivante va activer l’événement au lancement de l’application :
java -XX:StartFlightRecording=jdk.CPUTimeSample#enabled=true,filename=profile.jfr ...
514: Ahead-of-Time Command-Line Ergonomics
La JEP 483: Ahead-of-Time Class Loading & Linking a introduit en Java 24 la possibilité de créer un cache AOT (Ahead of Time – avant le lancement de l’application) contenant les classes déjà chargées et liées d’une application pour améliorer son temps de démarrage.
Cela devait se faire en trois étapes :
- Lancement de l’application pour enregistrer la liste des classes utilisées
- Création du cache AOT
- Lancement de l’application avec le cache AOT
Avec la JEP 514, ce processus est simplifié, il est maintenant faisable en deux étapes uniquement, les deux premières étapes pouvant être faites en une seule via la ligne de commande :
java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App ...
La liste des classes sera enregistrée pendant le fonctionnement de l’application, puis le cache AOT sera créé quand l’application sera arrêtée.
515: Ahead-of-Time Method Profiling
Le cache AOT présenté dans la section précédente s’enrichit des informations de profiling des méthodes !
Au démarrage d’une application Java, le Just-In-Time compiler (JIT) va profiler les méthodes Java pour savoir lesquels doivent être compilées (celle qui sont fréquemment utilisées) et guider ces optimisations via de nombreuses heuristiques (branches les plus utilisés, taille des boucles, …). C’est seulement après que ces informations de profiling auront été collectées, que le JIT va pouvoir compiler puis optimiser les classes de l’application.
La JEP 515 va étendre le cache AOT pour y ajouter les informations de profiling des méthodes, ce qui permettra au JIT de commencer avec des informations de profiling, et à l’application de démarrer plus rapidement et d’attendre plus rapidement ses performances de pointe.
Le JIT collectera toujours des statistiques au runtime.
Des tests préliminaires montrent une amélioration de performance de 19% d’un programme simple utilisant l’API Stream pour une augmentation de la taille du cache AOT de seulement 2.5%.
518: JFR Cooperative Sampling
Comme décrit dans la section à propos de la JEP 509; pour monitorer le temps d’exécution d’une méthode, JFR va sampler la stack d’exécution d’un programme à intervalles réguliers, par exemple toutes les 20ms. Il va donc collecter l’ensemble des stacks de tous les threads démarrés. Pour cela, il faut que les threads soient dans un état stable, cohérent les uns avec les autres.
Pour collecter les informations des threads, on peut utiliser le mécanisme de safepoint : la JVM demande à tous les threads de se suspendre, ceux-ci allant jusqu’à un point où ils peuvent se suspendre sans mettre en danger l’application : le safepoint. Ce mécanisme souffre de biais du safepoint : un thread loin d’un safepoint prendra du temps à se suspendre alors qu’un autre proche le fera rapidement.
Pour éviter ce biais, JFR scan les threads de manière asynchrone et les suspend en dehors d’un safepoint. Étant donné que les métadonnées des stacks de threads ne sont pas garanties comme étant valides en dehors des safepoints, JFR utilise des heuristiques afin de générer les stack des threads.
Ce mécanisme a été refait pour analyser les stacks de threads uniquement aux safepoints, évitant ainsi les erreurs. Pour contrer le biais de safepoint, JFR enregistre le pointeur de programme lors de la suspension, puis le thread continu et reconstitue stack à son prochain safepoint. Cela permet une collecte de données plus précise et moins risquée.
Plus d’information dans la JEP 518.
520: JFR Method Timing & Tracing
Ajoute le tracing et le timings des méthodes via instrumentation de bytecode à Java Flight Recorder (JFR).
Pour tracer les invocations de méthodes (tracing) : enregistre toutes statistiques d’invocations au lieu du mécanisme basé sur du sampling.
Pour mesurer les temps d’exécution des méthodes (timing) : permet la capture du temps d’exécution ainsi que la stack traces de méthodes spécifiques sans nécessiter de modification de code source.
Pour mesurer le temps d’exécution et tracer les invocations de méthodes, on utilise généralement lors du développement ou du débugging des outils tel que JMH (Java Micronbenchmark Harness), des sampling profilers ou des agents Java. Ceux-ci ne sont pas faits pour un usage en production et ont souvent un overhead non négligeable.
Avec la JEP 520, le JDK se dote d’un outil configurable et fait pour la production pour mesurer le temps d’exécution et tracer les invocations de méthodes !
Deux nouveaux événements jdk.MethodTiming
et jdk.MethodTrace
ont été ajoutés à JFR; ils sont configurables via un filtre qui permet de préciser quelles méthodes doivent être monitorées.
Par exemple, la commande suivante va activer le tracing de la méthode java.util.HashMap::resize
:
java -XX:StartFlightRecording:jdk.MethodTrace#filter=java.util.HashMap::resize,filename=recording.jfr ...
La ligne de commande suivante va activer le timing de tous les blocs d’initialisation statique :
java '-XX:StartFlightRecording:method-timing=::,filename=clinit.jfr' ...
Plus d’information dans la JEP 520.
Les fonctionnalités qui sortent de preview
Les fonctionnalités suivantes sortent de preview (ou du module incubator) et passent en standard :
- JEP 506 – Scoped Values : permettent le partage de données immuables au sein et entre les threads. Un changement :
ScopedValue.orElse
n’accepte plus null comme argument. - JEP 511 – Module Import Declarations : permet l’import de toutes les classes d’un module, transitivement, via l’instruction
import module java.base;
. - JEP 512 – Compact Source Files and Instance Main Methods : précédemment nommée Simple Source Files and Instance Main Methods, simplifie l’écriture de programmes simple en permettant de les définir dans une classe implicite (sans déclaration) et dans une méthode d’instance
void main()
. Simple Source File a été renommé en Compact Source File,java.io.IO
a été déplacé dans le packagejava.lang
et est donc importé implicitement dans chaque fichier source, les méthodes statiques de la classeIO
ne sont plus importées implicitement. - JEP 513 – Flexible Constructor Bodies : fonctionnalité qui autorise des instructions avant l’appel du constructeur parent tant que celles-ci n’accèdent pas à l’instance en cours de création.
- JEP 510 – Key Derivation Function API : nouvelle API pour les fonctions de dérivation de clés (Key Derivation Function – KDF), qui sont des algorithmes cryptographiques permettant de dériver des clés supplémentaires à partir d’une clé secrète et d’autres données.
- JEP 519 – Compact Object Headers : mode de la JVM qui réduit de 128 à 64 bits le header des objets, fait parti du projet Liliput.
- JEP 521 – Generational Shenandoah : ajoute un mode générationel au Garbage Collector Shenandoah.
Pour les détails sur celles-ci, vous pouvez vous référer à mes articles précédents.
Les fonctionnalités qui restent en preview
Les fonctionnalités suivantes restent en preview (ou en incubator module) :
- JEP 508 – Vector API : dixième incubation, API permettant d’exprimer des calculs vectoriels qui se compilent au moment de l’exécution en instructions vectorielles pour les architectures CPU prises en charge. Quelques changements dans l’API et des améliorations de performance. Il a été acté dans la JEP que la Vector API sera en incubation tant que les fonctionnalités du projet Valhalla ne seront pas disponibles en preview. Ce qui était attendu, car la Vector API pourra alors tirer parti des améliorations de performance et de représentation en mémoire que devrait apporter le projet Valhalla.
- JEP 505 – Structured Concurrency : cinquième preview, nouvelle API permettant de simplifier l’écriture de code multi-threadé en permettant de traiter plusieurs tâches concurrentes comme une unité de traitement unique. Il faut maintenant utiliser des méthodes factory static pour créer un
StructuredTaskScope
. - JEP 507 – Primitive Types in Patterns, instanceof, and switch : troisième preview, ajoute le support des types primitifs dans les
instanceof
et lesswitch
, et enrichit le pattern matching pour supporter des patterns de types primitifs : dans lesinstanceof
, dans les cases desswitch
, et dans la déconstruction d’un record. Aucun changement.
Pour les détails sur celles-ci, vous pouvez vous référer à mes articles précédents.
Divers
Divers ajouts au JDK :
ForkJoinPool
implémente désormaisScheduledExecutorService
- ZIP
Deflater
etInflater
implémentent désormaisAutoCloseable
ce qui permet de les utiliser dans un try-with-resources. Currency.availableCurrencies()
: retourne une stream des monnaies disponibles.TimeZone.availableIDs()
: retourne une stream des identifiants de time zone disponibles.TimeZone.availableIDs(int offset)
: retourne une stream des identifiants de time zone disponibles pour un offset donné.HttpResponse.BodyHandlers.limiting(BodyHandler, limit)
: retourne unBodyHandler
qui permet de limiter la taille du corps de la réponse.HttpResponse.BodySubscribers.limiting(BodySubscriber, limit)
: retourne unBodySubscriber
qui permet de limiter la taille du corps de la réponse.Math
etStrictMath
: ajout de méthodes pour la multiplication et la puissance en calcul exact (throw une exception s’il y a un overflow) :powExact()
,unsignedPowExact()
,unsignedMultiplyExact()
.CharBuffer
etCharSequence.getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
: copie les caractères d’une séquence spécifiée parsrcBegin
etsrcEnd
dans un tableau de caractère donné. Cette méthode a été implémentée de manière performante dans certaines classes enfants et a été utilisée dans la classeReader
pour en améliorer les performances.Reader.readAllAsString()
: lit tout les caractères restant en tant que String.- code>Reader.readAllLines() : lit tout les caractères restant en tant que liste de ligne de texte.
La totalité des nouvelles API du JDK 25 peuvent être trouvées dans The Java Version Almanac – New APIs in Java 25.
Des changements internes, de la performance, et de la sécurité
Comme toutes les nouvelles versions de Java, OpenJDK 25 contient son lot d’optimisation de performance et d’amélioration de sécurité.
Pas mal de changements côté JFR, une partie ont déjà été traités dans cet article mais vous pourrez trouver une liste exhaustive des ces changement dans l’article What’s new for JFR in JDK 25 d’Erik Gahlin.
Pour l’instant je n’ai pas listé d’autre changement significatif mais j’enrichirais cette section si nécessaire plus tard.
JFR Events
Voici les nouveaux événements Java Flight Recorder (JFR) de la JVM :
MethodTiming
: se référer à la section sur la JEP 520.MethodTrace
: se référer à la section sur la JEP 520.CPUTimeSample
: se référer à la section sur la JEP 509.CPUTimeSamplesLost
: se référer à la section sur la JEP 509.SafepointLatency
: délai pour qu’un thread atteigne un safepoint après une demande.JavaMonitorNotify
: notify sur un monitor Java.JavaMonitorDeflate
: pas de description.JavaMonitorStatistics
: pas de description.
Vous pouvez retrouver tous les événements JFR supportés dans cette version de Java sur la page JFR Events.
Conclusion
Au final, c’est une release de Java assez sympa, les additions à JFR sont conséquente et ont font un couteau suisse du monitoring de nos application Java. J’aime tout particulièrement la nouvelle API StableValue qui apporte simplicité et performance, ainsi que le support des texte PEM qui était un manque criant au JDK.
Comme toujours, on se languit du projet Valhalla…
Pour retrouver tous les changements de Java 25, se référer aux release notes.