TL;DR
- JDK 25 est le nouveau LTS (septembre 2025). C'est le baseline qui va dominer la production pour les 3-4 prochaines annees.
- Scoped Values finalisees : le remplacement propre de ThreadLocal, concu pour les threads virtuels.
- Compact Object Headers : reduction de la taille des headers d'objets de 12 a 8 octets. Moins de memoire, plus d'objets dans le heap.
- AOT method profiling : warmup plus rapide en reutilisant les profils JIT des executions precedentes.
- Flexible constructor bodies : enfin, du code avant
super(). Ca simplifie la validation dans les constructeurs.- Si tu es sur JDK 21, la migration est douce. Si tu es sur JDK 17, c'est le moment.
Pourquoi un LTS change la donne
Java sort une version tous les six mois. Mais en production, ce sont les LTS qui comptent. JDK 17 (sept 2021), JDK 21 (sept 2023), JDK 25 (sept 2025). Ce sont les versions que les equipes deploient, que les frameworks supportent en priorite, et que les entreprises validient pour la production.
JDK 25 est le premier LTS apres JDK 21. Ca veut dire 4 versions de features accumulees (JDK 22, 23, 24, 25) qui arrivent d'un coup dans l'ecosysteme enterprise. C'est beaucoup.
Les features finalisees
Scoped Values (JEP 487)
C'est la feature que j'attendais le plus. Les Scoped Values remplacent ThreadLocal pour le partage de donnees dans un scope d'execution.
Pourquoi ThreadLocal pose probleme :
- Les valeurs persistent au-dela de leur utilisation prevue (fuites memoire)
- Incompatible avec les threads virtuels (1 million de threads virtuels = 1 million de copies ThreadLocal)
- Mutabilite non controlee (n'importe qui peut modifier la valeur)
Les Scoped Values resolvent tout ca :
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
void handleRequest(User user) {
ScopedValue.runWhere(CURRENT_USER, user, () -> {
// Tout le code dans ce scope a acces a CURRENT_USER
processOrder();
sendNotification();
});
// Ici, CURRENT_USER n'est plus accessible
}
void processOrder() {
User user = CURRENT_USER.get(); // Lecture seule, pas de set()
// ...
}
Les differences cles avec ThreadLocal :
- Immutable dans le scope : pas de
set(), seulementget(). La valeur est definie a l'entree du scope. - Scope delimite : la valeur existe uniquement dans le
runWhere. Pas de fuite. - Performant avec les threads virtuels : pas de copie par thread, juste une reference dans la stack.
En pratique, ca remplace tous les patterns de "contexte de requete" (utilisateur courant, tenant, trace ID) de maniere plus propre et plus sure.
Compact Object Headers (JEP 450)
Chaque objet Java a un header de 12 octets (sur 64 bits avec compressed oops). Avec des millions d'objets en memoire, ca s'additionne. Les Compact Object Headers reduisent ca a 8 octets.
4 octets de moins par objet, ca semble derisoire. En realite, sur un heap de 4 GB avec des millions de petits objets (String, Integer, records), la reduction est significative :
Avant (JDK 21) : 12 octets header + donnees
Apres (JDK 25) : 8 octets header + donnees
Pour 10 millions d'objets :
Gain = 10M * 4 octets = 40 MB de heap recupere
Ca se traduit par :
- Moins de pression GC (moins de memoire utilisee = collections moins frequentes)
- Plus d'objets en cache L1/L2 (headers plus petits = meilleure localite)
- Reduction globale de l'empreinte memoire des applications Java
C'est une optimisation transparente -- tu ne changes rien a ton code, la JVM fait le travail.
AOT Method Profiling
La JVM HotSpot optimise ton code a l'execution grace au JIT compiler. Mais cette optimisation prend du temps -- le fameux "warmup" ou l'application est lente pendant les premieres minutes.
L'AOT method profiling resout ca en deux etapes :
- Pendant une execution : la JVM enregistre les profils de methodes (quelles methodes sont chaudes, quels types sont utilises)
- A l'execution suivante : la JVM charge ces profils et commence les optimisations JIT immediatement
# Etape 1 : enregistrer les profils
java -XX:+RecordTraining -XX:TrainingFile=app.training -jar myapp.jar
# Etape 2 : utiliser les profils
java -XX:+LoadTraining -XX:TrainingFile=app.training -jar myapp.jar
En pratique, ca reduit le temps de warmup de 30 a 60%. Pour les applications avec des contraintes de latence au demarrage (fonctions serverless, microservices avec autoscaling), c'est un vrai gain.
Flexible Constructor Bodies (JEP 482)
Depuis Java 1.0, la premiere instruction d'un constructeur doit etre super() ou this(). Aucune instruction avant. C'etait frustrant pour la validation :
// Avant JDK 25 : impossible
public Order(int quantity) {
if (quantity <= 0) throw new IllegalArgumentException("quantity must be positive");
super(quantity); // ERREUR : super() doit etre en premier
}
// Workaround laid : methode statique
public Order(int quantity) {
super(validate(quantity)); // Beurk
}
JDK 25 autorise du code avant super(), tant que tu n'accedes pas a this :
// JDK 25 : enfin propre
public Order(int quantity) {
if (quantity <= 0) throw new IllegalArgumentException("quantity must be positive");
super(quantity); // OK
}
C'est un petit changement syntaxique, mais ca elimine des dizaines de patterns detournes que tout dev Java a du ecrire au moins une fois.
Les features en preview
Structured Concurrency (5e preview)
Structured Concurrency est en preview depuis JDK 21. JDK 25 est la 5e iteration. Le concept : les taches concurrentes sont structurees dans un scope, comme des blocs de code.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<User> user = scope.fork(() -> fetchUser(id));
Subtask<List<Order>> orders = scope.fork(() -> fetchOrders(id));
scope.join();
scope.throwIfFailed();
return new UserProfile(user.get(), orders.get());
}
// Les deux taches sont automatiquement nettoyees ici
Si une tache echoue, l'autre est annulee automatiquement. Pas de thread qui traine, pas de fuite de ressources. C'est la concurrence telle qu'elle aurait toujours du fonctionner.
Apres 5 previews, la finalisation est attendue pour JDK 26 ou 27. En attendant, tu peux l'utiliser avec --enable-preview.
Primitive Types in Patterns
Les patterns de pattern matching supportent maintenant les types primitifs :
switch (statusCode) {
case 200 -> "OK";
case 404 -> "Not Found";
case int i when i >= 500 -> "Server Error: " + i;
case int i -> "Unknown: " + i;
}
Ca unifie le traitement des primitifs et des objets dans les expressions switch et le pattern matching.
Migration depuis JDK 21
La migration JDK 21 vers JDK 25 est probablement la plus douce des migrations LTS recentes. Pas de changement de module system (comme JDK 9), pas de suppression de APIs massivement utilisees (comme JDK 11 avec JavaEE).
Les points d'attention :
- APIs supprimees : quelques methodes deprecated-for-removal dans
java.lang,java.util. Le compilateur te previent. - SecurityManager : supprime definitivement. Si tu l'utilisais encore (rare), il faut migrer.
- Encoding par defaut : UTF-8 est le defaut depuis JDK 18. Si tu n'as pas encore ajuste, c'est le moment.
- Dependances tierces : verifie la compatibilite de tes librairies avec JDK 25. Les plus matures (Guava, Jackson, Netty) sont generalement compatibles rapidement.
# Verifier la compatibilite avec jdeps
jdeps --multi-release 25 --jdk-internals myapp.jar
Le nouveau stack 2025
Avec JDK 25 et Spring Boot 4 qui arrivent quasi simultanement, le nouveau stack Java enterprise se dessine :
| Composant | Ancien baseline | Nouveau baseline |
|---|---|---|
| JDK | 17 ou 21 | 25 |
| Spring Boot | 3.x | 4.x |
| Jakarta EE | 9/10 | 11 |
| Hibernate | 6.x | 7.x |
| Jackson | 2.x | 3.x |
C'est un renouvellement complet. La derniere fois que Java a connu un changement aussi synchronise, c'etait le passage Java 8 vers Java 11. Et ca a pris des annees dans beaucoup d'entreprises.
Mon conseil : ne repete pas cette erreur. Commence la migration maintenant, en commencant par JDK 25 sur tes projets les moins critiques. Quand Boot 4 sort en GA, tu seras pret.
Faut-il migrer maintenant ?
Si tu es sur JDK 21 : oui, des que JDK 25 est GA. La migration est douce, les gains sont reels (Scoped Values, Compact Object Headers, AOT profiling). Tu peux migrer le JDK independamment de Spring Boot.
Si tu es sur JDK 17 : commence a planifier. JDK 17 reste supporte, mais tu accumules du retard. La migration 17 vers 25 est plus lourde (2 LTS de saut). Prevois un sprint dedie.
Si tu es sur JDK 11 ou avant : c'est une urgence. Le support communautaire est termine, les patchs de securite se rarefient. Planifie une migration par etapes : 11 vers 17, puis 17 vers 25.
JDK 25 est le nouveau baseline. C'est la version que les frameworks ciblent, que les clouds optimisent, et que les recruteurs cherchent. Autant y aller.