actu-image
Software engineering FR - 05 Nov 2026

Practice Java chez MARGO : sept façons d’exécuter la même application, du JIT à GraalVM

Une même application Java peut démarrer en quelques millisecondes ou plafonner très vite. En effet, tout dépend de la manière dont elle est compilée puis exécutée. C’est précisément le fil rouge suivi par notre Practice Java lors de sa journée du 22 octobre.

Guidés par Houda, nos consultants ont ainsi testé sept configurations différentes. Ils sont passés de l’interpréteur classique à l’exécutable natif GraalVM. Par la suite, ils ont analysé les entrailles de la JVM avec les outils du JDK. Voici donc notre retour d’expérience complet.

Java Journée Practice Présentation Java Journée Practice Présentation Java Journée Practice Présentation

Compilé ou interprété : le JIT refuse de choisir

Il existe traditionnellement deux grandes familles de langages. D’un côté, les langages compilés comme C++ ou Rust traduisent directement le code en instructions machine. Dès lors, le démarrage est immédiat. Toutefois, le compilateur ignore le comportement réel du programme à l’exécution.

D’un autre côté, les langages interprétés comme Python s’exécutent sans compilation préalable. Cependant, ce confort se paie par des performances globales plus modestes.

Heureusement, le compilateur JIT (Just-In-Time) de la JVM réconcilie ces deux approches. L’application démarre d’abord en mode interprété. Ensuite, le système compile les méthodes les plus sollicitées pendant l’exécution. Au sein de HotSpot, cette montée en charge suit plusieurs paliers précis :

  • Niveau 0 (Interpréteur) : Le bytecode s’exécute immédiatement sans aucune compilation en code natif.
  • Niveaux 1 à 3 (Compilateur C1) : Le compilateur produit rapidement un code natif fonctionnel. De plus, les niveaux 2 et 3 collectent des profils d’exécution détaillés.
  • Niveau 4 (Compilateur C2) : Ce niveau s’appuie sur les profils collectés. Ainsi, il génère un code hautement optimisé sur les chemins critiques.

C’est pourquoi le JIT surpasse souvent une compilation purement statique. En effet, il optimise le code à partir des données réelles observées en production. De surcroît, il peut dé-optimiser si une hypothèse devient invalide. Néanmoins, cette flexibilité a un coût : le temps de chauffe (warmup).


Quand le temps de chauffe devient un problème

Pour un serveur longue durée, quelques secondes de chauffe restent négligeables. En revanche, ce délai devient pénalisant dans plusieurs architectures modernes :

  • Les architectures serverless : Le moindre démarrage à froid (cold start) dégrade directement la latence utilisateur.
  • Les outils en ligne de commande (CLI) : Le programme se termine souvent avant même l’intervention du JIT.
  • Les conteneurs contraints en ressources : Les threads de compilation monopolisent un CPU déjà limité au détriment du métier.
  • L’autoscaling sur le cloud : Une nouvelle instance doit être opérationnelle immédiatement lors d’un pic de trafic.

Par conséquent, la question n’est plus seulement de savoir comment compiler. Il faut désormais déterminer quand compiler et avec quelles informations préalables.


Sept configurations pour une même application

Pour répondre à ce défi, Houda nous a présenté GraalVM. Ce JDK dispose de son propre compilateur et produit des exécutables natifs autonomes. Dès lors, nous l’avons utilisé comme laboratoire d’expérimentation. Nous avons ainsi comparé sept approches en faisant varier le moment de la compilation :

# Configuration Activation Production du code natif Bénéfice principal Limite majeure
1 Interpréteur seul -Xint Jamais Référence pour mesurer l’apport du JIT Performances très faibles
2 Tier 1 uniquement -XX:TieredStopAtLevel=1 Pendant l’exécution (C1) Compilation rapide sans profilage Absence d’optimisations C2
3 Compilateur Graal Défaut sur GraalVM Pendant l’exécution (Graal remplace C2) Optimisations avancées (escape analysis) Temps de chauffe persistant
4 Compilateur C2 -XX:-UseJVMCICompiler Pendant l’exécution (C1 puis C2) Compilateur historique de HotSpot Temps de chauffe persistant
5 Native Image native-image -jar app.jar À la construction (Build time) Démarrage instantané et binaire autonome Monde fermé (déclaration du code dynamique)
6 Native Image avec PGO --pgo-instrument puis --pgo À la construction guidée par profil Débit proche d’un compilateur JIT Oracle GraalVM et build en deux étapes
7 Cache AOT (Leyden) -XX:AOTCache=app.aot Classes et profils préparés en amont Démarrage réduit sans quitter HotSpot Exige un scénario d’entraînement fidèle

Parmi ces options, deux technologies méritent une attention particulière :

D’abord, Native Image change radicalement les règles. L’outil analyse l’application en amont et conserve uniquement le code atteignable. Par conséquent, la réflexion et le chargement dynamique doivent être déclarés explicitement. C’est une contrainte sur des bases existantes, même si Quarkus ou Micronaut l’automatisent largement. De son côté, le PGO apporte la connaissance du trafic réel via une exécution instrumentée préalable.

Ensuite, Project Leyden adopte la démarche inverse. Au lieu de quitter la JVM, ce projet déplace une partie du travail vers une phase d’entraînement. En effet, cette trajectoire progresse de version en version dans OpenJDK :

  • JEP 483 (JDK 24) : Chargement et liaison anticipés des classes.
  • JEP 514 et 515 (JDK 25) : Simplification du cache et profils d’exécution intégrés.
  • JEP 516 (JDK 26) : Cache d’objets compatible avec l’ensemble des Garbage Collectors.
  • JEP 544 (proposée pour JDK 28) : Stockage direct du code machine optimisé dans le cache partagé.

Ouvrir le capot avec jcmd et jstack

Comparer des chronomètres ne suffit pas pour comprendre. C’est pourquoi nous avons ausculté l’application en temps réel avec les outils natifs du JDK :

jstack                   # Liste les threads, dont ceux des compilateurs
jcmd  Compiler.codecache # Affiche le volume de code natif généré
jcmd  Compiler.queue     # Liste les méthodes en attente de compilation

En pratique, l’affichage des threads révèle instantanément le mode de compilation utilisé. Voici un récapitulatif des observations sur HotSpot :

Mode d’exécution Threads de compilation dans jstack Code natif dans jcmd
Par défaut C1 CompilerThread0 et C2 CompilerThread0 Méthodes compilées par C1 et C2
-Xint Aucun thread actif nmethods=0
-XX:TieredStopAtLevel=1 C1 CompilerThread0 uniquement Méthodes compilées par C1
-XX:-TieredCompilation C2 CompilerThread0 uniquement Méthodes compilées par C2

Le point clé de l’observabilité native : Côté Native Image, il n’y a plus de JIT actif puisque tout est figé à la compilation. Toutefois, l’exécutable peut répondre aux commandes jcmd s’il a été compilé avec l’option adéquate :

native-image --enable-monitoring=jcmd -jar app.jar
jcmd  Thread.print

En d’autres termes, l’observabilité d’un binaire natif se décide impérativement dès sa construction.


Ce que nous en retenons

En conclusion de ces tests, il n’existe pas de solution universelle. En réalité, chaque architecture impose son propre compromis. Voici donc une matrice décisionnelle pour orienter vos choix :

Votre contexte applicatif Piste prioritaire Justification technique
Service à longue durée de vie JIT avec C2 ou Graal Le temps de chauffe s’amortit et l’optimisation s’adapte à la charge réelle.
Application existante à accélérer Cache AOT (Project Leyden) Aucune réécriture du code et comportement HotSpot 100 % préservé.
Fonction serverless ou CLI Native Image Démarrage instantané et empreinte mémoire minimale.
Binaire natif sous charge continue Native Image avec PGO Débit maximisé grâce aux profils d’exécution réels.
Script ou batch d’exécution courte -XX:TieredStopAtLevel=1 Un simple paramètre limite immédiatement le coût CPU de compilation.

Ainsi, trois principes directeurs guideront désormais nos projets chez nos clients :

  • Mesurer avant de choisir : Définissez votre priorité entre latence, mémoire, débit et temps de démarrage.
  • Soigner la phase d’entraînement : Le PGO et Leyden dépendent entièrement de la représentativité du trafic fourni.
  • Vérifier sous le capot : Utilisez les outils du JDK pour valider concrètement le comportement de vos runtimes.
Notre vision de l’accompagnement tech

Le Pôle EXCELLENCE : l’incubateur des talents de la tech de demain

Cette journée Practice Java reflète la dynamique de fond qui guide notre collectif. Chez MARGO, l’excellence ne s’acquiert pas mais se cultive chaque jour. C’est pourquoi le Pôle EXCELLENCE a été conçu directement avec nos consultants, dans une démarche de co-construction continue.

Véritable incubateur d’experts de la complexité, sa mission est d’offrir un cadre sur mesure pour accélérer durablement les carrières. Cette approche place le management par les pairs au cœur de notre modèle. Par conséquent, MARGO réinvente les standards du conseil technologique.

Chaque consultant intègre l’une de nos six Practices (Software Engineering, Cloud Transformation, Capital Markets Technologies, Artificial Intelligence, Data & Architecture, Insurance Digital Transformation). Dès lors, il s’appuie sur quatre leviers déterminants :

  • Un suivi de carrière par les pairs : Vous échangez d’égal à égal avec un Practice Manager expert de votre domaine. Ainsi, votre progression technique reste parfaitement alignée sur vos ambitions.
  • Une montée en compétences illimitée par la preuve : Notre écosystème garantit au moins une formation certifiante par an. De surcroît, MARGO finance des coachings réguliers et des Learning Expeditions.
  • L’investissement individuel valorisé : Vous animez des ateliers techniques et participez aux recrutements de futurs pairs. En outre, nos prises de parole publiques forgent votre leadership.

Rejoindre MARGO, c’est intégrer un collectif d’élite résolu à conjuguer rigueur d’ingénierie et esprit d’équipe.

Notre objectif : vous aider à maîtriser la complexité à travers l’excellence.

Rejoindre la communauté MARGO
Quelle est la différence fondamentale entre le JIT et l’AOT ?

Le compilateur JIT compile le bytecode en code machine pendant l’exécution. Ainsi, il tire parti du comportement observé en temps réel. En revanche, la compilation AOT (Ahead-Of-Time) prépare ce travail avant le démarrage. Dès lors, elle élimine le temps de chauffe et accélère le lancement initial.

GraalVM remplace-t-il complètement le JDK standard ?

GraalVM constitue un JDK complet et conforme aux spécifications Java. En effet, il peut exécuter des applications classiques sur sa JVM avec son propre compilateur. De plus, il intègre l’outil Native Image pour générer des exécutables natifs autonomes.

Faut-il privilégier Native Image ou Project Leyden ?

Le choix dépend avant tout de vos contraintes d’architecture. D’un côté, Native Image produit des exécutables autonomes très légers, mais impose l’analyse en monde fermé. De l’autre côté, Project Leyden conserve HotSpot et toute sa flexibilité dynamique tout en réduisant le démarrage grâce à un cache préparé.

Comment vérifier le compilateur actif sur une application Java en production ?

Il suffit d’exécuter la commande jstack pour identifier les threads de compilation actifs (C1 ou C2). Par la suite, la commande jcmd Compiler.codecache vous permet de visualiser précisément le volume de code natif généré.