JavaRush /Blog Java /Random-FR /Comment les classes sont chargées dans la JVM
Aleksandr Zimin
Niveau 1
Санкт-Петербург

Comment les classes sont chargées dans la JVM

Publié dans le groupe Random-FR
Une fois la partie la plus difficile du travail d'un programmeur terminée et l'application « Hello World 2.0 » écrite, il ne reste plus qu'à assembler le kit de distribution et à le transférer au client, ou au moins au service de test. Dans la distribution, tout est comme il se doit, et lorsque nous lançons notre programme, la machine virtuelle Java entre en scène. Ce n'est un secret pour personne que la machine virtuelle lit les commandes présentées dans les fichiers de classe sous forme de bytecode et les traduit sous forme d'instructions au processeur. Je propose de comprendre un peu le schéma d'entrée du bytecode dans la machine virtuelle.

Chargeur de classe

Il est utilisé pour fournir du bytecode compilé à la JVM, qui est généralement stocké dans des fichiers avec l'extension .class, mais peut également être obtenu à partir d'autres sources, par exemple téléchargé sur le réseau ou généré par l'application elle-même. Comment les classes sont chargées dans la JVM - 1Selon la spécification Java SE, pour que le code s'exécute dans la JVM, vous devez effectuer trois étapes :
  • charger le bytecode à partir des ressources et créer une instance de la classeClass

    Cela inclut la recherche de la classe demandée parmi celles chargées précédemment, l'obtention du bytecode pour le chargement et la vérification de son exactitude, la création d'une instance de la classe Class(pour travailler avec elle au moment de l'exécution) et le chargement des classes parentes. Si les classes parentes et les interfaces n'ont pas été chargées, alors la classe en question est considérée comme non chargée.

  • liaison (ou liaison)

    Selon le cahier des charges, cette étape est divisée en trois étapes supplémentaires :

    • Vérification , l'exactitude du bytecode reçu est vérifiée.
    • Préparation , allouant de la RAM aux champs statiques et les initialisant avec des valeurs par défaut (dans ce cas, l'initialisation explicite, le cas échéant, se produit déjà au stade de l'initialisation).
    • Résolution , résolution de liens symboliques de types, champs et méthodes.
  • initialiser l'objet reçu

    ici, contrairement aux paragraphes précédents, tout semble clair sur ce qui devrait se passer. Il serait bien sûr intéressant de comprendre exactement comment cela se produit.

Toutes ces étapes sont effectuées séquentiellement avec les exigences suivantes :
  • La classe doit être entièrement chargée avant d'être liée.
  • Une classe doit être entièrement testée et préparée avant d’être initialisée.
  • Des erreurs de résolution de lien se produisent lors de l'exécution du programme, même si elles ont été détectées au stade de la liaison.
Comme vous le savez, Java implémente le chargement paresseux (ou paresseux) des classes. Cela signifie que le chargement des classes de champs de référence de la classe chargée ne sera effectué que lorsque l'application rencontrera une référence explicite à ces derniers. En d’autres termes, la résolution des liens symboliques est facultative et ne se produit pas par défaut. Cependant, l'implémentation JVM peut également utiliser le chargement de classe énergétique, c'est-à-dire tous les liens symboliques doivent être pris en compte immédiatement. C’est sur ce point que s’applique la dernière exigence. Il convient également de noter que la résolution des liens symboliques n’est liée à aucune étape du chargement des classes. En général, chacune de ces étapes constitue une bonne étude ; essayons de comprendre la première, à savoir le chargement du bytecode.

Types de chargeurs Java

Il existe trois chargeurs standards en Java, chacun chargeant une classe à partir d'un emplacement spécifique :
  1. Bootstrap est un chargeur de base, également appelé Primordial ClassLoader.

    charge les classes JDK standard à partir de l'archive rt.jar

  2. Extension ClassLoader – chargeur d'extensions.

    charge les classes d'extension, qui se trouvent par défaut dans le répertoire jre/lib/ext, mais peuvent être définies par la propriété système java.ext.dirs

  3. System ClassLoader – chargeur système.

    charge les classes d'application définies dans la variable d'environnement CLASSPATH

Java utilise une hiérarchie de chargeurs de classes, où la racine est bien entendu celle de base. Vient ensuite le chargeur d'extension, puis le chargeur système. Bien entendu, chaque chargeur stocke un pointeur vers le parent afin de pouvoir lui déléguer le chargement au cas où il ne pourrait pas le faire lui-même.

ClassLoader de classe abstraite

Chaque chargeur, à l'exception de celui de base, est un descendant de la classe abstraite java.lang.ClassLoader. Par exemple, l'implémentation du chargeur d'extension est la classe sun.misc.Launcher$ExtClassLoader, et le chargeur système est sun.misc.Launcher$AppClassLoader. Le chargeur de base est natif et son implémentation est incluse dans la JVM. Toute classe qui s'étend java.lang.ClassLoaderpeut fournir sa propre façon de charger des classes avec du blackjack et ces mêmes. Pour ce faire, il est nécessaire de redéfinir les méthodes correspondantes, que je ne peux pour l'instant considérer que superficiellement, car Je n'ai pas compris ce problème en détail. Les voici:
package java.lang;
public abstract class ClassLoader {
    public Class<?> loadClass(String name);
    protected Class<?> loadClass(String name, boolean resolve);
    protected final Class<?> findLoadedClass(String name);
    public final ClassLoader getParent();
    protected Class<?> findClass(String name);
    protected final void resolveClass(Class<?> c);
}
loadClass(String name)l'une des rares méthodes publiques qui constitue le point d'entrée pour le chargement des classes. Son implémentation se résume à appeler une autre méthode protégée loadClass(String name, boolean resolve), qui doit être remplacée. Si vous regardez la Javadoc de cette méthode protégée, vous pouvez comprendre quelque chose comme ceci : deux paramètres sont fournis en entrée. L'un est le nom binaire de la classe (ou le nom de classe complet) qui doit être chargé. Le nom de la classe est spécifié avec une liste de tous les packages. Le deuxième paramètre est un indicateur qui détermine si une résolution de lien symbolique est requise. Par défaut, c'est false , ce qui signifie que le chargement paresseux des classes est utilisé. Ensuite, selon la documentation, dans l'implémentation par défaut de la méthode, il y a un appel findLoadedClass(String name)qui vérifie si la classe a déjà été chargée précédemment et, si c'est le cas, renvoie une référence à cette classe. Sinon, la méthode de chargement de classe du chargeur parent sera appelée. Si aucun des chargeurs ne parvient à trouver une classe chargée, chacun d'eux, dans l'ordre inverse, tentera de trouver et de charger cette classe, en remplaçant le fichier findClass(String name). Ceci sera discuté plus en détail dans le chapitre « Schéma de chargement de classe ». Et enfin, enfin et surtout, une fois la classe chargée, en fonction de l' indicateur de résolution , il sera décidé si les classes doivent être chargées via des liens symboliques. Un exemple clair est que l' étape Résolution peut être appelée pendant l'étape de chargement de la classe. Par conséquent, en étendant la classe ClassLoaderet en remplaçant ses méthodes, le chargeur personnalisé peut implémenter sa propre logique pour transmettre le bytecode à la machine virtuelle. Java prend également en charge le concept de chargeur de classe « actuel ». Le chargeur actuel est celui qui a chargé la classe en cours d'exécution. Chaque classe sait avec quel chargeur elle a été chargée, et vous pouvez obtenir cette information en appelant son String.class.getClassLoader(). Pour toutes les classes d'applications, le chargeur "actuel" est généralement celui du système.

Trois principes de chargement de classe

  • Délégation

    La demande de chargement de la classe est transmise au chargeur parent et une tentative de chargement de la classe elle-même n'est effectuée que si le chargeur parent n'a pas pu trouver et charger la classe. Cette approche vous permet de charger des classes avec le chargeur le plus proche possible de celui de base. Cela permet d’obtenir une visibilité maximale de la classe. Chaque chargeur conserve un enregistrement des classes qu'il a chargées et les place dans son cache. L’ensemble de ces classes est appelé portée.

  • Visibilité

    Le chargeur ne voit que « ses » classes et les classes « parent » et n'a aucune idée des classes qui ont été chargées par son « enfant ».

  • Unicité

    Une classe ne peut être chargée qu'une seule fois. Le mécanisme de délégation garantit que le chargeur qui lance le chargement de la classe ne surcharge pas une classe précédemment chargée dans la JVM.

Ainsi, lors de l’écriture de son bootloader, un développeur doit être guidé par ces trois principes.

Schéma de chargement de classe

Lorsqu'un appel pour charger une classe se produit, cette classe est recherchée dans le cache des classes déjà chargées du chargeur actuel. Si la classe souhaitée n'a pas été chargée auparavant, le principe de délégation transfère le contrôle au chargeur parent, qui se situe un niveau plus haut dans la hiérarchie. Le chargeur parent essaie également de trouver la classe souhaitée dans son cache. Si la classe a déjà été chargée et que le chargeur connaît son emplacement, alors un objet Classde cette classe sera renvoyé. Sinon, la recherche se poursuivra jusqu'à ce qu'elle atteigne le chargeur de démarrage de base. Si le chargeur de base ne dispose pas d'informations sur la classe requise (c'est-à-dire qu'elle n'a pas encore été chargée), le bytecode de cette classe sera recherché à l'emplacement des classes que le chargeur donné connaît, et si la classe ne peut pas être chargé, le contrôle reviendra au chargeur enfant, qui tentera de charger à partir de sources qu'il connaît. Comme mentionné ci-dessus, l'emplacement des classes pour le chargeur de base est la bibliothèque rt.jar, pour le chargeur d'extensions - le répertoire avec les extensions jre/lib/ext, pour celui du système - CLASSPATH, pour celui de l'utilisateur, cela peut être quelque chose de différent . Ainsi, la progression du chargement des classes va dans la direction opposée - du chargeur racine au chargeur actuel. Lorsque le bytecode de la classe est trouvé, la classe est chargée dans la JVM et une instance du type est obtenue Class. Comme vous pouvez facilement le voir, le schéma de chargement décrit est similaire à l'implémentation de la méthode ci - dessus loadClass(String name). Ci-dessous, vous pouvez voir ce diagramme dans le diagramme.
Comment les classes sont chargées dans la JVM - 2

Comme conclusion

Dans les premières étapes de l'apprentissage d'une langue, il n'est pas particulièrement nécessaire de comprendre comment les classes sont chargées en Java, mais connaître ces principes de base vous aidera à éviter le désespoir lorsque vous rencontrerez des erreurs telles que ClassNotFoundExceptionou NoClassDefFoundError. Eh bien, ou du moins, comprenez approximativement quelle est la racine du problème. Ainsi, une exception ClassNotFoundExceptionse produit lorsqu'une classe est chargée dynamiquement pendant l'exécution du programme, lorsque les chargeurs ne peuvent trouver la classe requise ni dans le cache, ni le long du chemin de classe. Mais l'erreur NoClassDefFoundErrorest plus critique et se produit lorsque la classe requise était disponible lors de la compilation, mais n'était pas visible lors de l'exécution du programme. Cela peut se produire si le programme a oublié d'inclure la bibliothèque qu'il utilise. Eh bien, le fait même de comprendre les principes de la structure de l'outil que vous utilisez dans votre travail (pas nécessairement une immersion claire et détaillée dans ses profondeurs) ajoute une certaine clarté à la compréhension des processus qui se déroulent à l'intérieur de ce mécanisme, qui, en à son tour, conduit à une utilisation confiante de cet outil.

Sources

Comment fonctionne ClassLoader en Java Dans l'ensemble, c'est une source très utile avec une présentation accessible des informations. Chargement de classes, ClassLoader Un article assez long, mais avec un accent sur la façon de créer votre propre implémentation de chargeur avec ces mêmes. ClassLoader : chargement dynamique des classes Malheureusement, cette ressource n'est pas disponible pour le moment, mais j'y ai trouvé le diagramme le plus compréhensible avec un schéma de chargement de classe, je ne peux donc m'empêcher de l'ajouter. Spécification Java SE : Chapitre 5. Chargement, liaison et initialisation
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION