Découvrez les 15 JEP de Java 21 LTS : threads virtuels, ZGC générationnel, pattern matching. Benchmarks de performances comparatifs avec Java 17.
JOptimize Team
Sorti le 19 septembre 2023, Java 21 est la nouvelle version Long Term Support (LTS) du JDK. Après deux ans séparant Java 17 (LTS de septembre 2021) de cette nouvelle mouture, la plateforme embarque pas moins de 15 JEPs (Java Enhancement Proposals) qui touchent à la fois au langage, à la JVM et aux performances. Pour les équipes encore sur Java 17, la question ne se pose plus : voici pourquoi et comment migrer.
C'est LA fonctionnalité phare de Java 21, issue du projet Loom d'OpenJDK. Après deux previews en Java 19 et 20, les threads virtuels sont désormais stables et prêts pour la production.
Avant Java 21, chaque thread Java était mappé 1:1 à un thread système (OS thread), coûteux en mémoire (~1 Mo par thread) et limité en scalabilité. Les threads virtuels brisent ce couplage : ils sont gérés par la JVM, multiplexés sur un petit pool de carrier threads, et ne consomment que quelques kilo-octets de mémoire chacun.
// Avant Java 21 — thread pool classique ExecutorService executor = Executors.newFixedThreadPool(200); // Avec Java 21 — threads virtuels, un par tâche ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); // Lancement direct d'un thread virtuel Thread.startVirtualThread(() -> { // opération I/O bloquante — le thread virtuel se démonte du carrier String data = fetchFromDatabase(userId); process(data); });
Le principe est simple : lorsqu'un thread virtuel rencontre une opération bloquante (I/O, appel base de données, appel HTTP), il se démonte du carrier thread et le libère pour d'autres tâches. Une fois l'opération terminée, il se remonte sur n'importe quel carrier disponible.
⚠️ Attention au pinning : l'utilisation de blocs
synchronizedà l'intérieur d'un thread virtuel peut l'épingler (pinned) au carrier thread, annulant le bénéfice. PréférezReentrantLockdans ce contexte.
// À éviter dans un thread virtuel synchronized (lock) { Thread.sleep(1000); // thread virtuel épinglé ! } // À privilégier ReentrantLock lock = new ReentrantLock(); lock.lock(); try { Thread.sleep(1000); // thread virtuel peut se démonter } finally { lock.unlock(); }
Intégration Spring Boot 3.2 : une seule ligne dans application.properties suffit pour activer les threads virtuels sur Tomcat :
spring.threads.virtual.enabled=true
Après 4 previews (Java 17, 18, 19 et 20), le Pattern Matching pour les expressions switch est officiellement standard en Java 21. Il permet de tester le type d'un objet directement dans les branches case, avec support des gardes (when) et des record patterns imbriqués.
// Java 17 — verbeux et répétitif Object obj = getShape(); if (obj instanceof Circle c) { return Math.PI * c.radius() * c.radius(); } else if (obj instanceof Rectangle r) { return r.width() * r.height(); } else { throw new IllegalArgumentException(); } // Java 21 — concis, expressif et exhaustif double area = switch (getShape()) { case Circle c -> Math.PI * c.radius() * c.radius(); case Rectangle r -> r.width() * r.height(); case Triangle t when t.isEquilateral() -> (Math.sqrt(3) / 4) * t.side() * t.side(); default -> throw new IllegalArgumentException(); };
Également finalisés en Java 21, les record patterns permettent de déconstruire un record directement dans un instanceof ou un switch, donnant accès à ses composants sans cast manuel.
record Point(int x, int y) {} record Line(Point start, Point end) {} // Déconstruction imbriquée en Java 21 if (shape instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) { double length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); System.out.println("Longueur : " + length); }
Combinée au Pattern Matching pour switch, cette fonctionnalité ouvre la voie au Data-Oriented Programming (DOP) en Java, permettant de se concentrer sur le domaine métier plutôt que sur le code d'infrastructure.
Java 21 introduit trois nouvelles interfaces pour représenter les collections ordonnées avec un ordre de rencontre défini :
SequencedCollection<E> — accès et manipulation au premier et dernier élémentSequencedSet<E> — ensemble ordonné sans doublonsSequencedMap<K,V> — map avec ordre de clés définiSequencedCollection<String> list = new ArrayList<>(List.of("a", "b", "c")); list.getFirst(); // "a" list.getLast(); // "c" list.addFirst("z"); // ["z", "a", "b", "c"] list.reversed(); // vue inversée : ["c", "b", "a", "z"]
Fini les list.get(list.size() - 1) ou les itérations complètes pour accéder au dernier élément d'une LinkedList !
Lorsqu'une variable n'est pas utilisée dans une lambda, un catch ou une boucle, il est désormais possible d'utiliser _ (underscore) pour l'indiquer explicitement :
// Lambda qui n'utilise pas son paramètre map.computeIfAbsent(id, _ -> new ArrayList<>()); // Exception capturée mais ignorée try { int i = Integer.parseInt(str); } catch (NumberFormatException _) { logger.warn("Valeur non numérique"); } // Variable de boucle non utilisée for (Element _ : elements) { counter++; }
Deux fonctionnalités du projet Loom restent en preview dans Java 21 :
ThreadLocal pour partager des données immuables avec des threads enfants, de manière sécurisée et performante.// Structured Concurrency — Java 21 Preview try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Future<User> user = scope.fork(() -> fetchUser(userId)); Future<Order> order = scope.fork(() -> fetchOrder(orderId)); scope.join(); // attend les deux tâches scope.throwIfFailed(); // propage la première erreur return new Response(user.resultNow(), order.resultNow()); }
Selon les benchmarks d'Azul, OpenJDK 21 affiche une performance 12% supérieure à la ligne de base, contre seulement 6% pour OpenJDK 17. Le gain est donc doublé entre les deux versions LTS. Ces améliorations proviennent d'optimisations continues du compilateur JIT (C2), de meilleures optimisations d'échappement, de l'inlining et de l'élimination de code mort.
Dans les benchmarks Timefold Solver (utilisant JMH avec une marge d'erreur de ±2%), la majorité des cas de test montrent une amélioration modeste mais constante lors du passage de Java 17 à Java 21. Les benchmarks Renaissance Suite avec 16 distributions Java 21 confirment cette tendance générale.
L'impact des threads virtuels est particulièrement spectaculaire pour les applications I/O-bound (appels base de données, services REST, microservices). Les chiffres parlent d'eux-mêmes :
| Métrique | Platform Threads | Virtual Threads |
|---|---|---|
| Mémoire par thread | ~1 Mo | ~quelques Ko |
| Threads simultanés (typique) | quelques milliers | millions |
| Throughput à charge I/O haute | dégradation rapide | quasi-constant |
| Changements de contexte OS | fréquents et coûteux | minimaux |
Concrètement : là où un serveur traditionnel plafonne à quelques milliers de threads concurrents avant de saturer, les threads virtuels maintiennent un débit quasi-constant jusqu'à 1 million de tâches simultanées, grâce au mécanisme de continuation interne à la JVM.
💡 Les threads virtuels ne bénéficient pas aux workloads CPU-bound. Ils brillent exclusivement sur les tâches bloquantes (I/O réseau, requêtes SQL, appels d'API externes).
Java 21 introduit le Generational ZGC, une évolution majeure du garbage collector ZGC qui divise le tas en deux générations :
# Activer le ZGC Générationnel en Java 21 java -XX:+UseZGC -XX:+ZGenerational -Xmx4g -jar monapp.jar
Les résultats des benchmarks SPECjbb montrent :
Netflix a mesuré les effets en production : dans le pire cas évalué, le ZGC non-générationnel consommait 36% de CPU en plus que G1 pour la même charge. Avec le ZGC Générationnel, cela se traduit par une amélioration de ~10% de l'utilisation CPU. Par ailleurs, des benchmarks sur Apache Cassandra montrent 4x le throughput avec seulement un quart de la taille de tas par rapport au ZGC non-générationnel, tout en maintenant des pauses sous 1 ms.
| Critère | Java 17 | Java 21 |
|---|---|---|
| Version LTS | ✅ Septembre 2021 | ✅ Septembre 2023 |
| Support Oracle (standard) | Jusqu'en 2026 | Jusqu'en 2028 (2031 avec support étendu) |
| Virtual Threads | ❌ Non disponibles | ✅ Stables (JEP 444) |
| Pattern Matching switch | Preview | ✅ Standard (JEP 441) |
| Record Patterns | Preview | ✅ Standard (JEP 440) |
| Sequenced Collections | ❌ | ✅ Standard (JEP 431) |
| ZGC Générationnel | ❌ | ✅ Disponible (JEP 439) |
| Perf. JVM (benchmark Azul) | +6% vs baseline | +12% vs baseline |
La réponse est oui, en particulier si votre application :
La migration depuis Java 17 est d'autant plus fluide que les deux versions sont LTS. La majorité des frameworks majeurs (Spring Boot 3.2+, Quarkus 3.x, WildFly 30+) supportent déjà pleinement Java 21 et ses threads virtuels.
spring.threads.virtual.enabled=truejdk.VirtualThreadPinned-XX:+UseZGC -XX:+ZGenerationalJava 21 n'est pas une mise à jour cosmétique. Avec les threads virtuels qui bouleversent la concurrence, le ZGC Générationnel qui réduit drastiquement les pauses GC, et des fonctionnalités de langage comme le Pattern Matching, les Record Patterns et les Sequenced Collections, cette version LTS marque une véritable modernisation de la plateforme Java.
Les benchmarks confirment : Java 21 est objectivement plus rapide, plus scalable et plus expressif que Java 17. La migration est accessible, bien supportée par l'écosystème, et les gains sont mesurables dès les premières semaines.
🚀 Vous souhaitez mesurer précisément les gains de performance de votre migration Java 17 → 21 ?
jOptimize vous aide à profiler vos applications Java en production, détecter les goulots d'étranglement JVM, analyser les comportements GC et optimiser vos configurations de threads virtuels. Commencez votre analyse gratuite dès aujourd'hui sur joptimize.io.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.