Learn · concepts
Qu'est-ce qu'un chargeur de classes Java ?
Un chargeur de classes trouve et charge les classes Java dans la JVM au moment de l'exécution. Voici comment ils fonctionnent, pourquoi la hiérarchie est importante et pourquoi les mods en dépendent.
Un chargeur de classes est la partie de l'environnement d'exécution Java qui trouve un fichier de classe et le charge dans la JVM. Chaque fois que votre code accède à un type pour la première fois, la JVM demande à un chargeur de classes : « Trouve cette classe pour moi. » Le chargeur localise le bytecode, le lit, et la JVM définit la classe en mémoire. Ce mécanisme est invisible dans les programmes Java normaux, mais c'est le crochet exact que les frameworks de mod utilisent pour modifier Minecraft.
Ce qu'un chargeur de classes fait réellement
Un chargeur de classes transforme un nom de classe en une classe utilisable à l'intérieur de la JVM en cours d'exécution. Vous demandez un type, le chargeur trouve le fichier .class correspondant (ou le génère), le vérifie et l'associe, et la classe est prête à être utilisée.
Le travail se déroule en trois étapes :
- Recherche — Le chargeur localise le bytecode pour la classe nommée (à partir d'un JAR, du disque, du réseau ou généré à la volée).
- Vérification et association — La JVM vérifie que le bytecode est valide et résout les références aux autres classes.
- Utilisation — La classe est prête et votre programme peut l'instancier.
C'est pourquoi les programmes Java peuvent charger dynamiquement du code au moment de l'exécution qui n'a jamais été mentionné au moment de la compilation. Le nom de la classe suffit pour que le chargeur puisse le trouver.
La hiérarchie des chargeurs de classes
Les chargeurs de classes sont organisés en une chaîne parentale, et la plupart d'entre eux demandent d'abord à leur parent. Une application Java standard possède trois chargeurs de classes intégrés :
| Chargeur | Charges | But |
|---|---|---|
| Bootstrap | Classes d'exécution Java de base | Garantit que les types de base tels que java.lang.Object sont toujours les mêmes |
| Plateforme | Modules de bibliothèque standard en dehors du noyau | Modules et extensions Java |
| Application | Les classes propres à votre programme | Le classpath que votre programme spécifie |
Lorsque le chargeur d'application est interrogé pour une classe, il délègue normalement vers le haut en premier. Il demande au chargeur de plateforme, qui demande au chargeur bootstrap, qui tente de la charger. Ce n'est qu'à défaut de pouvoir la charger par un parent que l'enfant essaie. C'est le modèle de délégation, et il empêche la même classe d'être chargée deux fois sous des noms différents.
Identité des classes et son fonctionnement
Une classe est identifiée à la fois par son nom et par le chargeur qui l'a définie. Deux chargeurs différents peuvent chacun charger une classe appelée com.example.MyClass, et la JVM les considère comme deux types complètement différents. Cela peut sembler obscur, mais c'est le fondement de tout ce que Java peut faire d'intéressant.
Bootstrap Loader: java.lang.Object (the core Object)
Application Loader: java.lang.Object (if somehow re-loaded by app loader)
→ Two different types, same name
→ This breaks your program, which is why delegation prevents it
Chargeurs de classes personnalisés
Les programmes peuvent définir leurs propres chargeurs de classes. Un chargeur personnalisé peut lire le bytecode à partir de n'importe où : un fichier JAR, le réseau, une base de données ou le bytecode qu'il a généré ou transformé à la volée. C'est ainsi :
- Les systèmes de plugins chargent du code dynamiquement sans le connaître au moment de la compilation.
- Les serveurs d'applications (Tomcat, JBoss) exécutent plusieurs applications dans la même JVM, chacune avec son propre chargeur de classes.
- Les mods Minecraft modifient les classes du jeu au moment de leur chargement.
Pourquoi cela importe pour les mods Minecraft
Les mods existent grâce au chargement de classes personnalisé. Un chargeur de mods pour Minecraft lance le jeu via son propre chargeur de classes personnalisé afin de pouvoir voir et modifier les classes du jeu au moment de leur chargement. C'est à ce point d'interception que la magie opère.
Lorsqu'un chargeur se situe entre le disque et la JVM, il peut transformer une classe juste avant que la JVM ne la définisse :
Disk: obfuscated Minecraft class
↓
Mod Loader (custom classloader)
↓ [bytecode transformation happens here]
↓
JVM: defines the patched class
↓
Memory: game runs with mods active
Un mod peut réécrire une méthode du jeu, injecter un nouveau comportement, ajouter des champs, le tout sans toucher le fichier JAR du jeu sur le disque. La classe que la JVM exécute est celle que le framework de mod a modifiée. C'est exactement ainsi que Fabric, Forge et les autres chargeurs de mods patch Minecraft.
Chargement paresseux
Toutes les classes ne sont pas chargées au démarrage. La JVM charge chaque classe la première fois qu'elle est réellement utilisée. On appelle cela le chargement paresseux :
Program starts → JVM doesn't load String class yet
User calls System.out.println() → String is needed → Loader loads it
Cela signifie qu'un chargeur de classes n'a besoin de trouver et de charger que les classes que votre code touche réellement. Les classes inutilisées n'entrent jamais en mémoire. C'est pourquoi le démarrage de votre Minecraft peut prendre un certain temps (les mods ajoutent beaucoup de classes) mais ne se fige pas à jamais.
FAQ
Lors de la première utilisation, et non au démarrage. La première fois que votre code accède à un type, la JVM demande à un chargeur de classe de le trouver et de le définir. C'est ce qu'on appelle le chargement paresseux, et c'est pourquoi le code inutilisé n'est jamais chargé en mémoire.
Un chargeur de classes demande à son parent de charger une classe avant d'essayer de le faire lui-même. La chaîne parentale remonte jusqu'au chargeur amorceur. Cela empêche la même classe d'être chargée deux fois sous le même nom par différents chargeurs.
Oui, si différents chargeurs de classes les définissent. La JVM identifie une classe par son nom et son chargeur définissant, de sorte que les classes de même nom provenant de chargeurs distincts sont considérées comme des types distincts.
Pour intercepter les classes au moment de leur chargement et les transformer en mémoire. Un chargeur personnalisé se situe entre le disque et la JVM, modifiant le bytecode avant que la JVM ne définisse la classe. C'est ainsi que les mods patch Minecraft sans modifier le fichier jar du jeu.