Les deux étapes fondamentales d'un programme Java
Le parcours d'un programme Java de sa source à son exécution implique deux phases majeures :
- Le fichier source (extension .java) est compilé en code octet (extension .class) par le compilateur.
- Le code octet est interprété et exécuté par la machine virtuelle Java (JVM).
La compilation génère un fichier .class contenant principalement :
- La table des constantes : elle inclut tous les tokens (noms de classes, de champs, etc.) et les références symboliques (vers des méthodes, des champs, etc.).
- Le code des méthodes : le code octet spécifique à chaque méthode de la classe.
L'exécution par la JVM se décompose en :
- Le chargement des classes.
- L'exécution du code des classes chargées.
Détail du processus d'exécution (à l'intérieur d'une méthode)
Considérons les fichiers sources suivants :
// Fichier: Vehicule.java
public class Vehicule {
private String marque;
public Vehicule(String marque) {
this.marque = marque;
}
public void afficherMarque() {
System.out.println("Vehicule [" + marque + "]");
}
}
// Fichier: ApplicationPrincipale.java
public class ApplicationPrincipale {
public static void main(String[] args) {
Vehicule maVoiture = new Vehicule("Renault");
maVoiture.afficherMarque();
}
}
Voici la séquence détaillée de l'exécution :
- Après compilation, l'exécution de
ApplicationPrincipaledémarre un processus JVM. La JVM localise le fichierApplicationPrincipale.classvia le classpath et charge ses informations de classe dans la zone méthodes de la mémoire d'exécution. C'est le chargement de la classe ApplicationPrincipale. - La JVM identifie le point d'entrée de la méthode
mainet commence son exécution. - La première instruction est la création d'une instance de
Vehicule. La zone méthodes ne contient pas encore les informations de la classeVehicule, la JVM déclenche donc immédiatement son chargement. - Une fois la classe
Vehiculechargée, la JVM alloue de l'espace dans le tas (heap) pour une nouvelle instance. L'objet créé contient une référence vers la structure de données de la classeVehiculedans la zone méthodes, incluant la table des méthodes (base du dynamisme Java). - Lors de l'appel à
afficherMarque(), la JVM suit la référence de l'objetmaVoiture, puis la référence interne de cet objet vers la table des méthodes de la classeVehiculedans la zone méthodes pour obtenir l'adresse du code de la méthode. - La méthode
afficherMarque()est alors exécutée.
Le chargement des classes (Class Loading)
Le chargement consiste à lire les données binaires d'un fichier .class en mémoire, à les placer dans la zone méthodes, puis à créer un objet java.lang.Class dans le tas. Cet objet Class encapsule la structure de données de la classe dans la zone méthodes et fournit une interface pour y accéder depuis le code Java.
Les phases du cycle de vie
De son chargement à son déchargement, une classe traverse sept phases : Chargement (Loading), Vérification (Verification), Préparation (Preparation), Résolution (Resolution), Initialisation (Initialization), Utilisation (Using), Déchargement (Unloading). Les trois phases centrales (Vérification, Préparation, Résolution) forment la connexion (Linking).
1. Chargement (Loading)
- Les octets du fichier .class sont lus en mémoire.
- La sturcture statique des données est transformée en une structure d'exécution dans la zone méthodes.
- Un objet
java.lang.Classreprésentant cette classe est généré dans le tas, servant de point d'accès aux données.
Note : un échec mémoire (OOM) dans la zone méthodes est souvent dû à un excès de classes chargées.
2. Connexion (Linking)
Vérification : S'assure que le code octet est conforme aux spécifications de la JVM et ne présente pas de risque pour la sécurité du runtime.
Préparation : Alloue de l'espace pour les variables statiques dans la zone méthodes et leur affecte une valeur par défaut (ex: static int compteur = 5; devient compteur = 0 à cette étape). Les vraiables d'instance ne sont pas concernées ici.
Résolution : Remplace les références symboliques (chaînes de caractères comme java.util.ArrayList) dans la table des constantes par des références directes (pointeurs mémoire ou offsets).
3. Initialisation (Initialization)
C'est la dernière phase du chargement. Elle exécute la méthode du constructeur de classe <clinit>(), qui agrège toutes les affectations de variables statiques et les blocs static{} écrits dans le code source. C'est ici que les valeurs initiales correctes sont réellement assignées aux variables statiques de la classe.
4. Utilisation et Déchargement
Utilisation : La classe est utilisée normalement par le programme.
Déchargement : Le ramasse-miettes (GC) peut éventuellement retirer de la mémoire les classes qui ne sont plus référencées.
Les chargeurs de classes (Class Loaders)
Le processus de chargement est effectué par des composants appelés chargeurs de classes.
Le modèle standard comprend :
- Bootstrap ClassLoader : Charge les classes essentielles de la bibliothèque standard (ex: rt.jar). Il est écrit en C++ et n'est pas une instance de
java.lang.ClassLoader. - Extension ClassLoader : Charge les classes des extensions de la plateforme Java (jars dans les répertoires spécifiés).
- Application ClassLoader : Charge les classes de l'application, depuis le classpath (classpath).
- Chargeurs Personnalisés : Des implémentations spécifiques (comme dans Tomcat ou JBoss) créées pour répondre à des besoins particuliers.
Lors du chargement d'une classe, le système vérifie d'abord si elle n'a pas déjà été chargée. Cette vérification s'effectue du bas vers le haut (du chargeur personnalisé vers le Bootstrap). Le chargement effectif, lui, se fait du haut vers le bas, chaque chargeur demandant à son parent de tenter le chargement avant de le faire lui-même. Ce mécanisme assure qu'une classe n'est chargée qu'une seule fois par chargeur.
Exemple de code pour explorer la hiérarchie des chargeurs :
public class ExplorationChargeurs {
public static void main(String[] args) {
ClassLoader chargeurActif = Thread.currentThread().getContextClassLoader();
System.out.println("Chargeur d'application : " + chargeurActif);
ClassLoader chargeurParent = chargeurActif.getParent();
System.out.println("Chargeur d'extension : " + chargeurParent);
ClassLoader chargeurGrandParent = chargeurParent.getParent();
System.out.println("Chargeur bootstrap : " + chargeurGrandParent); // Affiche null
}
}
Le résultat null pour le parent du chargeur d'extension est normal. Le Bootstrap ClassLoader, étant natif et non instancié comme un objet Java, n'a pas de représentation ClassLoader retournée. null indique qu'on a atteint le sommet de la hiérarchie.