W3docs

Architecture de la JVM Java

Structure de la JVM : chargeur de classes, zones de données d'exécution, moteur d'exécution et interface native.

La Java Virtual Machine (JVM) est le programme qui exécute votre programme. Vous compilez les sources .java en bytecode .class neutre vis-à-vis de la plateforme, et c'est la JVM qui charge ce bytecode, le vérifie, dispose ses objets en mémoire et l'exécute sur le matériel réel. « Écrire une fois, exécuter partout » signifie en réalité « compiler une fois, et laisser la JVM de chaque plateforme faire le reste. » Ce chapitre décrit la structure interne de la JVM — les trois sous-systèmes que toute implémentation partage — afin que les chapitres sur le modèle mémoire et le ramasse-miettes qui suivent disposent d'un cadre de référence.

Trois sous-systèmes

Une JVM, quel qu'en soit le fournisseur, est organisée en trois sous-systèmes coopérants. Tout le reste est un détail à l'intérieur de l'un d'eux.

Sous-systèmeResponsabilité
Chargeur de classesTrouve, charge, lie et initialise les fichiers .class dans l'environnement d'exécution
Zones de données d'exécutionLa mémoire gérée par la JVM : tas, piles, zone de méthodes, registres PC
Moteur d'exécutionInterprète et compile le bytecode en JIT, et exécute le ramasse-miettes

Le chargeur de classes importe les types, les zones de données d'exécution conservent l'état, et le moteur d'exécution exécute le code. Un appel de méthode touche les trois : sa classe est chargée, son frame est empilé sur une pile, et son bytecode est exécuté.

Le sous-système de chargement de classes

Les classes ne sont pas toutes chargées au démarrage. La JVM charge une classe de façon paresseuse, la première fois qu'elle est référencée, en trois phases : le chargement (lecture des octets), la liaison (vérification du bytecode, préparation des champs statiques, résolution des références), et l'initialisation (exécution des initialiseurs statiques et des blocs static { }).

Les chargeurs forment une hiérarchie parent-d'abord. Lorsqu'un chargeur reçoit une demande de classe, il délègue vers le haut à son parent avant d'essayer lui-même — ainsi, un type central comme String provient toujours du chargeur d'amorçage de confiance et ne peut jamais être masqué par le code applicatif.

// Walk the loader chain of any class
ClassLoader loader = MyType.class.getClassLoader();
while (loader != null) {
  System.out.println(loader.getName());
  loader = loader.getParent();
}
// A null result means the bootstrap loader (native, no Java object).
System.out.println(String.class.getClassLoader()); // prints: null

Les trois chargeurs standard, de l'enfant au parent, sont le chargeur applicatif (classpath), le chargeur de plateforme (modules JDK tels que java.sql), et le chargeur d'amorçage (noyau java.base, implémenté en code natif, représenté par null). Le chapitre sur le chargement de classes couvre ce cycle de vie et le modèle de délégation en détail ; les modules expliquent comment java.base et ses homologues sont empaquetés.

Les zones de données d'exécution

Une fois qu'une classe est chargée, son code et ses données résident dans des régions que la JVM partitionne à des fins distinctes :

  • Tas — partagé entre tous les threads ; chaque objet et tableau y réside. C'est ce que gère le ramasse-miettes.
  • Piles JVM — une par thread. Chaque appel de méthode empile un frame contenant des variables locales et des opérandes ; le frame est dépilé au retour. Une récursion trop profonde le déborde (StackOverflowError).
  • Zone de méthodes (Metaspace dans les JVM modernes) — métadonnées de classe, pool de constantes d'exécution et champs statiques. Mémoire hors tas.
  • Registre PC — par thread ; l'adresse de l'instruction bytecode en cours d'exécution.
// Heap allocation: 'new' carves space out of the heap
byte[] buffer = new byte[1024]; // lives on the heap, GC-managed

// Stack growth: each call adds a frame to this thread's stack
static long factorial(long n) {
  return n <= 1 ? 1 : n * factorial(n - 1); // each call = one more frame
}

La distinction tas / hors-tas est la séparation clé : les instances d'objets se trouvent sur le tas ; les structures de classes et la machinerie elle-même ne s'y trouvent pas. Le chapitre sur pile vs. tas compare ces deux régions en détail.

Le moteur d'exécution

Le moteur d'exécution transforme le bytecode en actions. Les JVM HotSpot modernes sont adaptatives : elles commencent par interpréter le bytecode (démarrage rapide), profilent les méthodes fréquemment exécutées, puis les confient au compilateur Just-In-Time (JIT), qui émet du code machine natif optimisé. Le code froid reste interprété ; le code chaud est compilé — vous ne payez le coût d'optimisation que là où cela se justifie.

Le moteur héberge également le ramasse-miettes, qui récupère les objets du tas qui ne sont plus accessibles depuis aucune référence active, et la Java Native Interface (JNI), le pont vers les bibliothèques écrites en C/C++.

// A hot loop: the JIT will compile sum() to native code after enough calls
long total = 0;
for (int i = 0; i < 100_000_000; i++) {
  total += i; // interpreted at first, then JIT-compiled, then much faster
}

Exemple concret : inspecter la JVM en cours d'exécution

Ce programme ne configure rien — il demande à la JVM en cours d'exécution de se décrire via les beans java.lang.management et l'API du chargeur de classes. Il nomme la VM et le moteur d'exécution, parcourt la hiérarchie des chargeurs, rend compte de la mémoire tas et hors-tas, et fait croître la pile par récursion.

java— editable, runs on the server

Ce que l'on retient de l'exécution :

  • Le RuntimeMXBean nomme le moteur d'exécution — une VM de la famille HotSpot (la ligne vm name affiche quelque chose comme 'OpenJDK 64-Bit Server VM') — confirmant que c'est le moteur « Server » capable de JIT qui exécute votre bytecode, et non un simple interpréteur.
  • La chaîne de chargeurs affiche quelque chose comme <unnamed> -> app -> platform -> bootstrap(null) : chaque itération de boucle a remonté vers le parent, et la chaîne s'est terminée à un parent null — le chargeur d'amorçage, qui est du code natif sans objet Java. La hiérarchie est réelle et observable, pas une métaphore.
  • String.class.getClassLoader() vaut null — les types java.base du noyau proviennent de ce même chargeur d'amorçage au sommet de la chaîne, ce qui explique précisément pourquoi le code applicatif ne peut jamais substituer son propre String. La délégation parent-d'abord fait son travail.
  • Les lignes mémoire montrent le tas utilisé en Ko mais un maximum de tas en Mo, et une valeur hors-tas séparée : les instances d'objets résident sur le tas géré par le GC, tandis que les métadonnées de classes (Metaspace) sont comptabilisées comme hors-tas — les deux régions sont suivies indépendamment.
  • depth(1) retourne 5 car chaque appel récursif a empilé son propre frame sur la pile JVM de ce thread et l'a dépilé au retour ; la pile est par thread et structurée en frames, c'est pourquoi une récursion incontrôlée se termine par StackOverflowError plutôt que de corrompre le tas.

Pratique

Pratique
Un programme Java référence la classe 'java.lang.String'. Dans la hiérarchie de chargeurs de classes parent-d'abord standard, quel chargeur la fournit en définitive, et comment ce chargeur apparaît-il depuis le code Java ?
Un programme Java référence la classe 'java.lang.String'. Dans la hiérarchie de chargeurs de classes parent-d'abord standard, quel chargeur la fournit en définitive, et comment ce chargeur apparaît-il depuis le code Java ?
Was this page helpful?