W3docs

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 :

ChargeurChargeRapporté comme
BootstrapClasses JDK principales (java.*, modules de base javax.*)null
PlateformeLe reste des modules de la plateforme JDKun PlatformClassLoader
Système / ApplicationVotre code depuis le classpath/module pathun 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();          // PlatformClassLoader

Le 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.
  • Liaisonvé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.

java— editable, runs on the server

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 : ClassLoadingDemo a été défini par un chargeur de niveau application dont getParent() 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() affiche null, la façon qu'a la JVM de dire "chargé par le chargeur bootstrap." Les types JDK principaux signalent toujours null ici ; un objet impliquerait qu'ils proviennent d'un chargeur inférieur, ce qui n'est jamais le cas.
  • app.loadClass("java.lang.StringBuilder") == StringBuilder.class est true. 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 objet Class, pas un doublon — preuve que la délégation empêche les types principaux d'être chargés deux fois.
  • Lazy <clinit> running s'affiche une seule fois, entre le marqueur --- referencing Lazy now --- et le premier Lazy.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).
  • a et b sont tous deux nommés Widget pourtant a == b est false et a.isAssignableFrom(b) est false. 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

Pratique
Deux chargeurs de classes personnalisés distincts chargent chacun un bytecode identique pour une classe nommée 'com.acme.Widget'. Qu'est-ce qui est vrai des objets Class résultants a (du chargeur 1) et b (du chargeur 2) ?
Deux chargeurs de classes personnalisés distincts chargent chacun un bytecode identique pour une classe nommée 'com.acme.Widget'. Qu'est-ce qui est vrai des objets Class résultants a (du chargeur 1) et b (du chargeur 2) ?

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éflexionClass.forName et loadClass sont les points d'entrée sur lesquels la réflexion s'appuie.
Was this page helpful?