Chargement de classes Java
Comment la JVM trouve et charge les classes avec les chargeurs de classes — bootstrap, plateforme, système et chargeurs personnalisés.
Avant que la JVM puisse exécuter une seule ligne de votre code, elle doit trouver le fichier .class, lire son bytecode, le vérifier et le transformer en un objet Class vivant en mémoire. Ce travail appartient à un chargeur de classes. Le chargement de classes permet à java.lang.String d'être disponible sans que vous ayez à faire quoi que ce soit, permet à un JAR sur le classpath d'apparaître à l'exécution, et alimente les systèmes de plugins, les serveurs d'applications et les outils de rechargement à chaud. Ce chapitre montre comment les chargeurs sont organisés, comment fonctionne la délégation et pourquoi l'identité d'une classe va au-delà de son simple nom.
La hiérarchie des chargeurs de classes
Les chargeurs sont organisés en une chaîne de parents, chacun responsable d'une source différente de classes. Sur un JDK moderne (9+), il existe trois chargeurs intégrés :
| Chargeur | Charge | Rapporté comme |
|---|---|---|
| Bootstrap | Classes JDK principales (java.*, modules de base javax.*) | null |
| Plateforme | Le reste des modules de la plateforme JDK | un PlatformClassLoader |
| Système / Application | Votre code depuis le classpath/module path | un AppClassLoader |
Chaque classe mémorise le chargeur qui l'a définie. Vous pouvez demander à n'importe quelle classe quel chargeur l'a produite :
ClassLoader appLoader = MyApp.class.getClassLoader(); // AppClassLoader
ClassLoader strLoader = String.class.getClassLoader(); // null = bootstrap
ClassLoader parent = appLoader.getParent(); // PlatformClassLoaderLe chargeur bootstrap est écrit en code natif, pas en Java, ce qui explique pourquoi String.class.getClassLoader() retourne null plutôt qu'un objet — il n'y a pas d'instance Java ClassLoader à renvoyer.
Le modèle de délégation
Les chargeurs de classes suivent le modèle de délégation parent-d'abord. Lorsqu'on lui demande de charger une classe, un chargeur n'essaie pas immédiatement de la trouver. Il demande d'abord à son parent, qui demande à son parent, jusqu'au bootstrap. Ce n'est que si aucun ancêtre ne peut fournir la classe que le chargeur original tente de la définir lui-même.
// Conceptual shape of ClassLoader.loadClass:
protected Class<?> loadClass(String name, boolean resolve) {
Class<?> c = findLoadedClass(name); // already loaded? reuse it
if (c == null) {
try {
c = parent.loadClass(name); // delegate UP first
} catch (ClassNotFoundException e) {
c = findClass(name); // only now load it myself
}
}
return c;
}Cette délégation garantit que les types fondamentaux sont chargés une seule fois, par le chargeur le plus haut capable de les fournir. C'est pourquoi vous ne pouvez pas remplacer java.lang.String en déposant votre propre String.class sur le classpath — le chargeur bootstrap revendique le nom en premier.
Chargement, liaison, initialisation
Donner vie à une classe se fait en trois phases, et ce ne sont pas la même chose :
- Chargement — lire le bytecode et créer l'objet
Class. - Liaison — vérifier que le bytecode est bien formé, préparer les champs statiques avec des valeurs par défaut, et résoudre les références symboliques.
- Initialisation — exécuter les initialiseurs statiques et les affectations de champs statiques (la méthode
<clinit>de la classe).
Le fait pratique essentiel : l'initialisation est paresseuse et se produit exactement une fois. Une classe n'est initialisée que lors de sa première utilisation active — le premier new, le premier appel de méthode statique, ou la première lecture d'un champ statique non constant.
class Config {
static final Map<String, String> SETTINGS = load(); // runs once, on first touch
static Map<String, String> load() {
System.out.println("Config initialized");
return Map.of("env", "prod");
}
}
// "Config initialized" prints only when Config is first actively used.Chargeurs de classes personnalisés
Vous pouvez étendre ClassLoader pour charger des classes depuis n'importe où — une base de données, un flux réseau, du bytecode généré, ou un JAR chiffré. Les deux méthodes qui importent sont findClass (localiser et définir les octets) et defineClass (remettre les octets bruts à la JVM, qui retourne un Class).
class BytesLoader extends ClassLoader {
private final byte[] bytecode;
BytesLoader(byte[] bytecode) { this.bytecode = bytecode; }
@Override
protected Class<?> findClass(String name) {
return defineClass(name, bytecode, 0, bytecode.length);
}
}URLClassLoader est la version intégrée de cette idée — pointez-le vers des JARs ou des répertoires et il charge les classes à la demande :
URL jar = Path.of("plugin.jar").toUri().toURL();
try (URLClassLoader loader = new URLClassLoader(new URL[]{ jar })) {
Class<?> plugin = loader.loadClass("com.example.Plugin");
Object instance = plugin.getDeclaredConstructor().newInstance();
}Identité de classe : nom plus chargeur
Voici la subtilité qui piège les développeurs : l'identité à l'exécution d'une classe est son nom qualifié complet et le chargeur qui l'a définie. Chargez les octets de Widget à travers deux chargeurs différents et vous obtenez deux objets Class distincts — pas égaux, pas compatibles en affectation — même si les deux proviennent d'un bytecode identique. C'est exactement ainsi que les serveurs d'applications isolent deux applications déployées qui contiennent toutes deux une classe nommée com.acme.Util.
Un exemple complet : chargeurs, délégation, paresse et identité
Ce programme n'a besoin d'aucune classe externe — il utilise les chargeurs déjà présents dans toute JVM. Il parcourt la chaîne de chargeurs, prouve que les classes principales viennent du chargeur bootstrap, montre la délégation retournant le même objet Class, observe un initialiseur statique se déclencher paresseusement et une seule fois, puis définit le même bytecode assemblé manuellement à travers deux chargeurs pour prouver la règle d'identité nom-plus-chargeur.
Ce qu'il faut retenir de l'exécution :
- La chaîne de chargeurs affichée est la hiérarchie de chargeurs de classes en direct avec votre code en bas :
ClassLoadingDemoa été défini par un chargeur de niveau application dontgetParent()est le chargeur suivant dans la hiérarchie. Chaque chargeur ne connaît que son parent, et la chaîne monte toujours vers le bootstrap. String.class.getClassLoader()affichenull, la façon qu'a la JVM de dire "chargé par le chargeur bootstrap." Les types JDK principaux signalent toujoursnullici ; un objet impliquerait qu'ils proviennent d'un chargeur inférieur, ce qui n'est jamais le cas.app.loadClass("java.lang.StringBuilder") == StringBuilder.classesttrue. La délégation a envoyé la requête au chargeur qui possède déjàStringBuilder, donc vous avez récupéré le même objetClass, pas un doublon — preuve que la délégation empêche les types principaux d'être chargés deux fois.Lazy <clinit> runnings'affiche une seule fois, entre le marqueur--- referencing Lazy now ---et le premierLazy.VALUE = 42, et jamais lors de la deuxième lecture. L'initialisation est paresseuse (elle a attendu la première utilisation) et idempotente (le bloc statique s'exécute exactement une fois par chargeur).aetbsont tous deux nommésWidgetpourtanta == bestfalseeta.isAssignableFrom(b)estfalse. Deux chargeurs ont défini le même bytecode en deux types distincts — preuve concrète que l'identité de classe à l'exécution est le nom qualifié complet plus le chargeur définissant, le mécanisme derrière l'isolation du classpath dans les serveurs d'applications.
Pratique
Sujets connexes
Le chargement de classes se situe à la frontière entre la JVM et votre code, il touche donc plusieurs sujets voisins :
- Architecture JVM — où le sous-système de chargement de classes s'inscrit parmi le moteur d'exécution et les zones de données d'exécution.
- Modèle mémoire Java — comment les classes chargées et leurs données statiques vivent en mémoire.
- Ramasse-miettes — les chargeurs de classes (et leurs classes) peuvent eux-mêmes être déchargés lorsqu'ils ne sont plus référencés.
- Introduction aux modules — sur le module path, le chargement est piloté par la lisibilité des modules plutôt que par un classpath plat.
- Introduction à la réflexion —
Class.forNameetloadClasssont les points d'entrée sur lesquels la réflexion s'appuie.