English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Comprendre en profondeur le processus de chargement des classes Java

Processus complet de chargement de la classe Java

Un fichier Java, du moment où il est chargé à ce qu'il soit déschargé, doit passer par4phases :

>Chargement->Liage (vérification+>Préparation+>Analyse->Initialisation (préparation avant l'utilisation)->Utilisation->Déschargement

où le chargement (à l'exception du chargement personnalisé)+Le processus de liage est entièrement géré par le JVM, quand il faut initialiser une classe (charger+Liage a été terminé auparavant), le JVM a des règles strictes (quatre cas) :

1. Lorsqu'il s'agit de new, getstatic, putstatic, invokestatic, ces4Lorsque le bytecode d'instruction est ajouté et que la classe n'a pas été initialisée, elle doit être initialisée immédiatement. En fait, c'est juste3Ce cas : utiliser new pour instancier une classe, lire ou définir un champ statique de la classe (à l'exclusion des champs statiques marqués final, qui ont été placés dans le pool de constantes) et exécuter une méthode statique.

2.Utilisation de java.lang.reflect.*.Lorsque le reflective method pour appeler une classe n'a pas été initialisé, il le fait immédiatement.

3.Lors de l'initialisation d'une classe, si son père n'a pas encore été initialisé, il ira d'abord initialiser son père.

4.Lors du démarrage de JVM, l'utilisateur doit spécifier une classe principale à exécuter (contenant static void main(String[] args) cette classe), alors JVM ira d'abord initialiser cette classe.

Ci-dessus4Cette préparation s'appelle une référence active à une classe, et toutes les autres situations, appelées références passives, ne déclenchent pas l'initialisation de la classe. Voici quelques exemples de références passives :

/** 
 * Scénario d'appel passif1 
 * L'appel d'un champ statique de la classe parente par la classe fille ne déclenche pas l'initialisation de la classe fille 
 * @author volador 
 * 
 */ 
class SuperClass{ 
  static{ 
    System.out.println("super class init."); 
  } 
  public static int value=123; 
} 
class SubClass extends SuperClass{ 
  static{ 
    System.out.println("sub class init."); 
  } 
} 
public class test{ 
  public static void main(String[]args){ 
    System.out.println(SubClass.value); 
  } 
} 

Le résultat de la sortie est : super class init.

/** 
 * Scénario d'appel passif2 
 * L'appel par l'intermédiaire d'un tableau ne déclenche pas l'initialisation de cette classe 
 * @author volador 
 * 
 */ 
public class test{ 
  public static void main(String[] args){ 
    SuperClass s_list=new SuperClass[10]; 
  } 
} 

Résultat de la sortie : aucune sortie

/** 
 * Scénario d'appel passif3 
 * Les constantes sont stockées dans le pool de constantes de l'appelant à la compilation, elles ne font pas référence à la classe définissant la constante, donc elles ne déclencheront pas l'initialisation de la classe définissant la constante 
 * @author root 
 * 
 */ 
class ConstClass{ 
  static{ 
    System.out.println("ConstClass init."); 
  } 
  public final static String value="hello"; 
} 
public class test{ 
  public static void main(String[] args){ 
    System.out.println(ConstClass.value); 
  } 
} 

Résultat de la sortie : hello (astuce : à la compilation, ConstClass.value a été transformé en constante hello et placé dans le pool de constantes de la classe test)

Ce qui précède est concernant l'initialisation des classes, les interfaces doivent également être initialisées, l'initialisation des interfaces est légèrement différente de l'initialisation des classes :

Le code ci-dessus utilise static{} pour afficher les informations d'initialisation, les interfaces ne peuvent pas le faire, mais le compilateur génère toujours un constructeur de classe constructeur <clinit>() pour les interfaces, utilisé pour initialiser les variables membres de l'interface, ce qui est également fait dans l'initialisation des classes. La véritable différence réside au troisième point, avant l'exécution de l'initialisation de la classe, il est nécessaire que toutes les classes parentes soient complètement initialisées, mais l'initialisation de l'interface semble ne pas être très intéressée par l'initialisation de l'interface parent, c'est-à-dire, lors de l'initialisation de l'interface enfant, il n'est pas nécessaire que l'interface parent soit complètement initialisée, mais seulement lorsqu'il est réellement utilisé (par exemple, en引用 les constantes de l'interface).

Analysons en détail le processus complet de chargement d'une classe : chargement->Vérification->Préparation->Analyse->Initialisation

Le premier est le chargement :

    C'est ce que le virtual machine doit accomplir3La deuxième chose :

        1.Obtenir le flux binaire binaire définissant cette classe à partir du nom complet de la classe.

        2.Transformer la structure de stockage statique représentée par ce flux de bytes en une structure de données de runtime de la zone de méthode.

        3.Générer dans la pile heap un objet java.lang.Class représentant cette classe, en tant que point d'accès aux données de la zone de méthode.

Concernant le premier point, c'est très flexible, beaucoup de technologies y entrent, car il n'y a pas de limitation sur l'origine du flux binaire :

venant d'un fichier class->Charger un fichier en général

venant d'un fichier zip->Charger les classes à partir d'un fichier jar

venant du réseau->Applet

..........

Compared to other stages of the loading process, the loading stage has the strongest controllability, because the class loader can be the system's or one you write yourself, the programmer can write a loader in his own way to control the acquisition of byte streams.

Après l'obtention du flux binaire, il sera stocké de la manière dont le jvm le nécessite dans la zone de méthode, en même temps, un objet java.lang.Class sera instancié dans la pile heap pour lier les données de la pile heap.

Après la charge, il faut commencer à vérifier ces flux de bytes (beaucoup de ces étapes sont croisées avec les étapes précédentes, par exemple la validation du format de fichier) :

Le but de la vérification : s'assurer que les informations du flux de bytes du fichier class sont conformes aux goûts du jvm, sans le rendre mal à l'aise. Si le fichier class est compilé à partir de code java pur, il ne devrait pas y avoir de problèmes sains tels que les dépassements d'array, les sauts vers des blocs de code inexistants, etc., car une fois que ces phénomènes se produisent, le compilateur refuse de compiler. Cependant, comme mentionné précédemment, le flux de fichier Class n'est pas nécessairement compilé à partir du code source java, il peut aussi provenir du réseau ou d'autres endroits, même vous pouvez vous-même utiliser16L'écriture en système de numération, si le jvm ne vérifie pas ces données, certains flux de bytes malveillants pourraient faire planter complètement le jvm.

Les vérifications passent principalement par plusieurs étapes : vérification du format du fichier->Vérification des métadonnées->Vérification des bytecode->Vérification des références symboliques

Vérification du format du fichier : vérifie si le flux de byte est conforme aux normes du format Class et si la version peut être traitée par la version actuelle de JVM. Bon, pas de problème, le flux de byte peut entrer dans la zone de méthode de la mémoire pour être enregistré. Les étapes suivantes sont3Ces vérifications sont toutes effectuées dans la zone de méthode.

Vérification des métadonnées : analyse sémantique des informations décrites dans les bytecode pour s'assurer qu'elles sont conformes aux normes syntaxiques du langage Java.

Vérification des bytecode : la plus complexe, vérifie le contenu du corps des méthodes pour s'assurer qu'elles ne feront pas quelque chose d'anormal au moment de l'exécution.

Vérification des références symboliques : pour vérifier l'authenticité et la faisabilité des références, par exemple, si d'autres classes sont mentionnées dans le code, il faut vérifier s'il existe réellement ces classes ; ou si d'autres attributs de classes sont accédés, il faut vérifier l'accessibilité de ces attributs. (Cette étape préparera la phase d'analyse suivante)

La phase de vérification est importante, mais pas nécessaire, si certains codes sont utilisés fréquemment et ont été vérifiés pour leur fiabilité, la phase d'implémentation peut essayer d'utiliser-Le paramètre Xverify:none désactive la plupart des mesures de vérification de la classe pour raccourcir le temps de chargement de la classe.

Après avoir terminé les étapes précédentes, nous entrerons dans la phase de préparation :

Cette phase alloue de la mémoire aux variables de classe (celles qui sont des variables statiques) et les initialise avec leurs valeurs initiales, cette mémoire étant allouée dans la zone de méthode. Il convient de noter que cette étape ne donne qu'une valeur initiale aux variables statiques, tandis que les variables d'instance sont allouées lors de l'instanciation des objets. L'initialisation des variables de classe diffère légèrement de l'assignment des variables de classe, par exemple :

public static int value=123;

à cette étape, la valeur de value sera 0 plutôt que123parce qu'à ce moment-là, aucune ligne de code Java n'a encore été exécutée,123reste invisible, tandis que ce que nous voyons est123L'instruction putstatic assignant la valeur à value est présente dans <clinit>() après la compilation du programme, donc, assigner la valeur à value est123Cela s'exécute uniquement lors de l'initialisation.

Il y a une exception ici :

public static final int value=123;

Ici, pendant la phase de préparation, la valeur de value sera initialisée à123Cela signifie que pendant la phase de compilation, javac génère une propriété ConstantValue pour cette valeur spéciale et, pendant la phase de préparation, jm assigne la valeur de cette ConstantValue à value.

Après avoir terminé l'étape précédente, il faut procéder à l'analyse. L'analyse semble être une conversion des champs, des méthodes et autres éléments de la classe, impliquant spécifiquement le format et le contenu du fichier Class, sans aller au fond des choses.

Le processus d'initialisation est la dernière étape du processus de chargement des classes:}

Dans le processus de chargement des classes précédent, à l'exception de la phase de chargement où l'utilisateur peut participer via des chargeurs de classes personnalisés, toutes les autres actions sont entièrement dirigées par le JVM, et à cette étape d'initialisation, le véritable exécution du code java commence.

Cette étape exécutera certaines opérations préparatoires, notez que lors de la phase de préparation, une affectation système des variables de classe a déjà été effectuée.

En réalité, cette étape consiste à exécuter la méthode <clinit>() du programme. Nous allons maintenant étudier la méthode <clinit>() :

La méthode <clinit>() est appelée méthode de constructeur de la classe, elle combine automatiquement toutes les opérations d'affectation des variables de classe et les instructions des blocs statiques, et les place dans l'ordre correspondant à leur position dans le fichier source.

La méthode <clinit>(); est différente de la méthode de constructeur de la classe, elle n'a pas besoin de faire appel explicitement à la méthode <clinit>(); de la classe parent, le JVM garantit que la méthode <clinit>(); de la classe enfant est exécutée après que la méthode de cette méthode de la classe parent ait été exécutée, c'est-à-dire, la méthode <clinit>() la première à être exécutée dans le JVM est celle de java.lang.Object.

Voici un exemple pour illustrer cela :

static class Parent{ 
  public static int A=1; 
  static{ 
    A=2; 
  } 
} 
static class Sub extends Parent{ 
  public static int B=A; 
} 
public static void main(String[] args){ 
  System.out.println(Sub.B); 
} 

Tout d'abord, Sub.B fait une référence aux données statiques, le classe Sub doit être initialisée. En même temps, le parent classe Parent doit être initialisée en premier. Après l'initialisation de Parent, A=2,donc B=2;Ce processus est équivalent à :

static class Parent{ 
  <clinit>(){ 
    public static int A=1; 
    static{ 
      A=2; 
    } 
  } 
} 
static class Sub extends Parent{ 
  <clinit>(){ //Le JVM permet d'exécuter d'abord la méthode de la classe parent avant de passer à cette étape 
  public static int B=A; 
  } 
} 
public static void main(String[] args){ 
  System.out.println(Sub.B); 
} 

La méthode <clinit>(); n'est pas nécessaire pour les classes et les interfaces. Si la classe ou l'interface n'affecte pas les variables de classe et n'a pas de blocs statiques, la méthode <clinit>() n'est pas générée par le compilateur.

Étant donné que les blocs statiques {} ne peuvent pas exister à l'intérieur de l'interface, mais il est toujours possible que des opérations d'affectation de variables se produisent lors de l'initialisation des variables, donc l'interface génère également un constructeur <clinit>(). Mais contrairement aux classes, il n'est pas nécessaire d'exécuter la méthode <clinit>(); de l'interface parent avant d'exécuter la méthode <clinit>(); de l'interface enfant. L'interface parent n'est initialisée que lorsque les variables définies dans l'interface parent sont utilisées.

De plus, la classe implémentant une interface neexécutera pas non plus la méthode <clinit>() de l'interface lors de l'initialisation.}

De plus, le JVM garantit que la méthode <clinit>(); d'une classe puisse être correctement verrouillée et synchronisée dans un environnement multithreadé. <Car l'initialisation ne sera exécutée qu'une seule fois>.

Voici un exemple pour illustrer cela :

public class DeadLoopClass { 
  static{ 
    if(true){ 
    System.out.println("["+Thread.currentThread()+"] A été initialisé, passons maintenant à une boucle infinie"); 
    while(treu){}   
    } 
  } 
  /** 
   * @param args 
   */ 
  public static void main(String[] args) { 
    // TODO Auto-généré par le biais d'un stub de méthode 
    System.out.println("toplaile"); 
    Runnable run=new Runnable(){ 
      @Override 
      public void run() { 
        // TODO Auto-généré par le biais d'un stub de méthode 
        System.out.println("["+Thread.currentThread()+"] Allons实例化 cette classe"); 
        DeadLoopClass d=new DeadLoopClass(); 
        System.out.println("["+Thread.currentThread()+"] A terminé l'initialisation de cette classe"); 
      }}; 
      new Thread(run).start(); 
      new Thread(run).start(); 
  } 
} 

Dans ce cas, vous verrez un phénomène de blocage lors de l'exécution.

Merci de lire, j'espère que cela pourra aider tout le monde, merci de votre soutien à ce site !

Vous pourriez aussi aimer