Back to Blog
Java 21Java 17JVMPerformanceVirtual ThreadsZGCPattern MatchingLTSMigrationSpring Boot

Java 21 vs Java 17 : nouveautés, performances et pourquoi migrer maintenant

Découvrez les 15 JEP de Java 21 LTS : threads virtuels, ZGC générationnel, pattern matching. Benchmarks de performances comparatifs avec Java 17.

J

JOptimize Team

April 23, 2026· 10 min read

Java 21 vs Java 17 : nouveautés, performances et pourquoi migrer maintenant

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.


Ce qui change dans le langage Java 21

Threads Virtuels (JEP 444) — La révolution de la concurrence

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érez ReentrantLock dans 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

Pattern Matching pour switch (JEP 441) — Finalisé !

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

Record Patterns (JEP 440) — Déconstruction des records

É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.


Sequenced Collections (JEP 431) — Enfin une API cohérente !

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ément
  • SequencedSet<E> — ensemble ordonné sans doublons
  • SequencedMap<K,V> — map avec ordre de clés défini
SequencedCollection<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 !


Variables sans nom — Unnamed Variables (JEP 443 — Preview)

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++; }

Scoped Values & Structured Concurrency (Preview)

Deux fonctionnalités du projet Loom restent en preview dans Java 21 :

  • Scoped Values (JEP 446) : alternative aux ThreadLocal pour partager des données immuables avec des threads enfants, de manière sécurisée et performante.
  • Structured Concurrency (JEP 453) : simplifie la gestion de tâches concurrentes liées en les traitant comme une seule unité de travail, avec annulation et propagation d'erreur automatiques.
// 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()); }

Performances : Java 21 vs Java 17, que disent les benchmarks ?

Amélioration générale de la JVM

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.

Threads virtuels : des gains x10 à x100 pour les workloads I/O

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étriquePlatform ThreadsVirtual Threads
Mémoire par thread~1 Mo~quelques Ko
Threads simultanés (typique)quelques milliersmillions
Throughput à charge I/O hautedégradation rapidequasi-constant
Changements de contexte OSfréquents et coûteuxminimaux

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).

ZGC Générationnel (JEP 439) : ~10% de throughput en plus

Java 21 introduit le Generational ZGC, une évolution majeure du garbage collector ZGC qui divise le tas en deux générations :

  • Jeune génération (Young Region) : collectée très fréquemment, car la plupart des objets y meurent rapidement (hypothèse de générations faibles).
  • Ancienne génération (Old Region) : collectée moins souvent, pour les objets à longue durée de vie.
# 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 :

  • +10% de throughput par rapport au ZGC non-générationnel de JDK 17
  • Pauses GC maintenues sous 1 milliseconde, même avec 275 clients concurrents (contre saturation dès 75 clients avec le ZGC legacy)
  • Moins de ressources CPU gaspillées en GC, donc plus disponibles pour l'application

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.

Résumé comparatif Java 17 vs Java 21

CritèreJava 17Java 21
Version LTS✅ Septembre 2021✅ Septembre 2023
Support Oracle (standard)Jusqu'en 2026Jusqu'en 2028 (2031 avec support étendu)
Virtual Threads❌ Non disponibles✅ Stables (JEP 444)
Pattern Matching switchPreview✅ Standard (JEP 441)
Record PatternsPreview✅ 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

Faut-il migrer de Java 17 à Java 21 ?

La réponse est oui, en particulier si votre application :

  • Gère de nombreuses requêtes concurrentes (API REST, microservices, serveurs)
  • Effectue de nombreuses opérations I/O (base de données, appels HTTP, fichiers)
  • Utilise Spring Boot 3.2+ (support natif des threads virtuels)
  • Souffre de latences GC ou de consommation mémoire élevée

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.

Étapes de migration recommandées

  1. Mettez à jour votre JDK vers Java 21 (Temurin, Corretto, Oracle GraalVM...)
  2. Lancez vos tests sans modification — la rétrocompatibilité est excellente
  3. Activez les threads virtuels dans Spring Boot : spring.threads.virtual.enabled=true
  4. Détectez les problèmes de pinning via JFR : cherchez les événements jdk.VirtualThreadPinned
  5. Expérimentez le ZGC Générationnel : -XX:+UseZGC -XX:+ZGenerational
  6. Profilez et mesurez vos métriques clés avant/après migration

Conclusion

Java 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.

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.