Maison  >  Article  >  Java  >  Introduction détaillée au mécanisme de chargement des classes Java

Introduction détaillée au mécanisme de chargement des classes Java

angryTom
angryTomavant
2019-08-15 16:32:122240parcourir

Introduction détaillée au mécanisme de chargement des classes Java

Article réimprimé de :http://www.pythonheidong.com/blog/article/1152/

Dans de nombreux Java Lors des entretiens, nous voyons souvent des inspections sur le mécanisme de chargement des classes Java, comme la question suivante :

class Grandpa{
    static
    {        System.out.println("爷爷在静态代码块");
    }
}    
class Father extends Grandpa{
    static
    {        System.out.println("爸爸在静态代码块");
    }

    public static int factor = 25;

    public Father()
    {        System.out.println("我是爸爸~");
    }
}class Son extends Father{
    static 
    {        System.out.println("儿子在静态代码块");
    }

    public Son()
    {        System.out.println("我是儿子~");
    }
}
public class InitializationDemo{
    public static void main(String[] args)
    {        System.out.println("爸爸的岁数:" + Son.factor);  //入口
    }
}

Veuillez écrire la chaîne de sortie finale.

La bonne réponse est :

爷爷在静态代码块
爸爸在静态代码块
爸爸的岁数:25

Je crois qu'après avoir vu cette question, les expressions de nombreux étudiants se sont effondrées et ils ne savaient plus par où commencer. Certains l'ont même rencontré à plusieurs reprises et n'ont toujours pas trouvé la bonne solution.

En fait, ce type de question d'entretien teste votre compréhension du mécanisme de chargement des classes Java.

Si vous ne comprenez pas le mécanisme de chargement de Java, vous ne pourrez pas répondre à cette question.

Donc, dans cet article, je vais d'abord vous amener à apprendre les connaissances de base du chargement de classes Java, puis j'analyserai quelques questions en pratique pour vous aider à saisir les idées.

Apprenons d'abord les sept étapes du mécanisme de chargement des classes Java.

Tutoriel recommandé : "Tutoriel vidéo Java"

Étape de sept mécanismes de chargement de classes Java

Lorsque notre code Java sera compilé, le fichier de classe correspondant sera généré. Ensuite, lorsque nous exécutons la commande java Demo, nous démarrons réellement la machine virtuelle JVM pour exécuter le contenu du fichier de bytecode de classe. Le processus de la machine virtuelle JVM exécutant le bytecode de classe peut être divisé en sept étapes : chargement, vérification, préparation, analyse, initialisation, utilisation et désinstallation.

Chargement

Ce qui suit est la description la plus officielle du processus de chargement.

La phase de chargement est la première phase du processus de chargement de classe. À ce stade, l'objectif principal de la JVM est de convertir le bytecode de divers emplacements (réseau, disque, etc.) en un flux d'octets binaires et de le charger en mémoire. Ensuite, un objet Class correspondant sera créé pour cette classe. la zone méthode de la JVM. Cet objet Class est l'entrée d'accès à diverses données de cette classe.

En fait, la phase de chargement peut être décrite en une phrase : charger les données de code en mémoire. Ce processus n'est pas directement lié à notre réponse à cette question, mais il s'agit d'un processus de mécanisme de chargement de classe, il doit donc être mentionné.

Vérification

Une fois que la JVM a chargé le fichier de bytecode de classe et créé l'objet de classe correspondant dans la zone de méthode, la JVM commencera la vérification du flux de bytecode. Seule la JVM ne lancera que les fichiers avec le bytecode. les spécifications peuvent être exécutées correctement par la JVM. Ce processus de vérification peut être grossièrement divisé selon les types suivants :

  • Vérification des spécifications JVM. JVM effectuera une vérification du format de fichier sur le flux d'octets pour déterminer s'il est conforme aux spécifications JVM et s'il peut être traité par la version actuelle de la machine virtuelle. Par exemple : si le fichier commence par 0x cafe bene, si les numéros de version majeure et mineure sont dans la plage de traitement de la machine virtuelle actuelle, etc.
  • Vérification de la logique du code. La JVM vérifiera le flux de données et le flux de contrôle composé du code pour garantir qu'aucune erreur fatale ne se produira après l'exécution du fichier de bytecode par la JVM. Par exemple, une méthode nécessite qu'un paramètre de type int soit transmis, mais lors de son utilisation, un paramètre de type String est transmis. Une méthode demandait de renvoyer un résultat de type String, mais au final aucun résultat n'a été renvoyé. Le code fait référence à une classe appelée Apple, mais vous ne définissez pas réellement la classe Apple.

Lorsque les données de code sont chargées dans la mémoire, la machine virtuelle vérifiera les données de code pour voir si le code est réellement écrit conformément aux spécifications JVM. Ce processus n'est pas directement lié à notre réponse à la question, mais il est nécessaire de connaître ce processus pour comprendre le mécanisme de chargement des classes.

Préparation (points clés)

Après avoir terminé la vérification du fichier de bytecode, la JVM commencera à allouer de la mémoire pour les variables de classe et à l'initialiser. Deux points clés sont à noter ici, à savoir l'objet mémoire alloué et le type d'initialisation.

  • Objet alloué en mémoire. Il existe deux types de variables en Java : les "variables de classe" et les "variables membres de classe". Les "variables de classe" font référence aux variables modifiées par statique, tandis que tous les autres types de variables appartiennent aux "variables membres de classe". Pendant la phase de préparation, la JVM allouera uniquement de la mémoire pour les « variables de classe » et non pour les « variables membres de classe ». L'allocation de mémoire des "variables membres de la classe" doit attendre la phase d'initialisation.

Par exemple, dans la phase de préparation du code suivant, la mémoire ne sera allouée que pour l'attribut factor, mais pas pour l'attribut website.

public static int factor = 3;public String website = "www.cnblogs.com/chanshuyi";
  • Le type initialisé. Pendant la phase de préparation, la JVM allouera de la mémoire pour les variables de classe et les initialisera. Mais l'initialisation fait ici référence à l'attribution à la variable de la valeur zéro du type de données dans le langage Java, et non de la valeur initialisée dans le code utilisateur.

Par exemple, dans le code suivant, après la phase de préparation, la valeur du secteur sera 0 au lieu de 3.

public static int sector = 3;

但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。

public static final int number = 3;

之所以 static final 会直接被复制,而 static 变量会被赋予零值。其实我们稍微思考一下就能想明白了。

两个语句的区别是一个有 final 关键字修饰,另外一个没有。而 final 关键字在 Java 中代表不可改变的意思,意思就是说 number 的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,因此被 final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。

解析

当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

其实这个阶段对于我们来说也是几乎透明的,了解一下就好。

初始化(重点)

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

看到上面几个条件你可能会晕了,但是不要紧,不需要背,知道一下就好,后面用到的时候回到找一下就可以了。

使用

当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。这个阶段也只是了解一下就可以。

卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。这个阶段也只是了解一下就可以。

看完了Java的类加载机智之后,是不是有点懵呢。不怕,我们先通过一个小例子来醒醒神。

public class Book {    public static void main(String[] args)    {
        System.out.println("Hello ShuYi.");
    }

    Book()
    {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }

    {
        System.out.println("书的普通代码块");
    }    int price = 110;    static
    {
        System.out.println("书的静态代码块");
    }    static int amount = 112;
}

思考一下上面这段代码输出什么?

给你5分钟思考,5分钟后交卷,哈哈。

怎么样,想好了吗,公布答案了。

书的静态代码块
Hello ShuYi.

怎么样,你答对了吗?是不是和你想得有点不一样呢。

下面我们来简单分析一下,首先根据上面说到的触发初始化的5种情况的第4种(当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类),我们会进行类的初始化。

那么类的初始化顺序到底是怎么样的呢?

重点来了!

重点来了!

重点来了!

在我们代码中,我们只知道有一个构造方法,但实际上Java代码编译成字节码之后,是没有构造方法的概念的,只有类初始化方法 和 对象初始化方法 。

那么这两个方法是怎么来的呢?

  • 类初始化方法。编译器会按照其出现顺序,收集类变量的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。

上面的这个例子,其类初始化方法就是下面这段代码了:

    static
    {
        System.out.println("书的静态代码块");
    }    static int amount = 112;
  • 对象初始化方法。编译器会按照其出现顺序,收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。

上面这个例子,其对象初始化方法就是下面这段代码了:

    {
        System.out.println("书的普通代码块");
    }    int price = 110;
    System.out.println("书的构造方法");
    System.out.println("price=" + price +",amount=" + amount);

类初始化方法 和 对象初始化方法 之后,我们再来看这个例子,我们就不难得出上面的答案了。

但细心的朋友一定会发现,其实上面的这个例子其实没有执行对象初始化方法。

因为我们确实没有进行 Book 类对象的实例化。如果你在 main 方法中增加 new Book() 语句,你会发现对象的初始化方法执行了!

感兴趣的朋友可以自己动手试一下,我这里就不执行了。

通过了上面的理论和简单例子,我们下面进入更加复杂的实战分析吧!

实战分析

class Grandpa{
    static
    {        System.out.println("爷爷在静态代码块");
    }
}    
class Father extends Grandpa{
    static
    {        System.out.println("爸爸在静态代码块");
    }

    public static int factor = 25;

    public Father()
    {        System.out.println("我是爸爸~");
    }
}class Son extends Father{
    static 
    {        System.out.println("儿子在静态代码块");
    }

    public Son()
    {        System.out.println("我是儿子~");
    }
}
public class InitializationDemo{
    public static void main(String[] args)
    {        System.out.println("爸爸的岁数:" + Son.factor);  //入口
    }
}

思考一下,上面的代码最后的输出结果是什么?

最终的输出结果是:

爷爷在静态代码块
爸爸在静态代码块
爸爸的岁数:25

也许会有人问为什么没有输出「儿子在静态代码块」这个字符串?

这是因为对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块)。因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

对面上面的这个例子,我们可以从入口开始分析一路分析下去:

  • 首先程序到 main 方法这里,使用标准化输出 Son 类中的 factor 类成员变量,但是 Son 类中并没有定义这个类成员变量。于是往父类去找,我们在 Father 类中找到了对应的类成员变量,于是触发了 Father 的初始化。
  • 但根据我们上面说到的初始化的 5 种情况中的第 3 种(当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化)。我们需要先初始化 Father 类的父类,也就是先初始化 Grandpa 类再初始化 Father 类。于是我们先初始化 Grandpa 类输出:「爷爷在静态代码块」,再初始化 Father 类输出:「爸爸在静态代码块」。
  • 最后,所有父类都初始化完成之后,Son 类才能调用父类的静态变量,从而输出:「爸爸的岁数:25」。

怎么样,是不是觉得豁然开朗呢。

我们再来看一下一个更复杂点的例子,看看输出结果是啥。

class Grandpa{
    static
    {        System.out.println("爷爷在静态代码块");
    }

    public Grandpa() {        System.out.println("我是爷爷~");
    }
}class Father extends Grandpa{
    static
    {        System.out.println("爸爸在静态代码块");
    }

    public Father()
    {        System.out.println("我是爸爸~");
    }
}class Son extends Father{
    static 
    {        System.out.println("儿子在静态代码块");
    }

    public Son()
    {        System.out.println("我是儿子~");
    }
}
public class InitializationDemo{
    public static void main(String[] args)
    {        new Son();  //入口
    }
}

输出结果是:

爷爷在静态代码块
爸爸在静态代码块
儿子在静态代码块
我是爷爷~
我是爸爸~
我是儿子~

怎么样,是不是觉得这道题和上面的有所不同呢。

让我们仔细来分析一下上面代码的执行流程:

  • 首先在入口这里我们实例化一个 Son 对象,因此会触发 Son 类的初始化,而 Son 类的初始化又会带动 Father 、Grandpa 类的初始化,从而执行对应类中的静态代码块。因此会输出:「爷爷在静态代码块」、「爸爸在静态代码块」、「儿子在静态代码块」。
  • 当 Son 类完成初始化之后,便会调用 Son 类的构造方法,而 Son 类构造方法的调用同样会带动 Father、Grandpa 类构造方法的调用,最后会输出:「我是爷爷~」、「我是爸爸~」、「我是儿子~」。

看完了两个例子之后,相信大家都胸有成足了吧。

下面给大家看一个特殊点的例子,有点难哦!

public class Book {    public static void main(String[] args)    {
        staticFunction();
    }    static Book book = new Book();    static
    {
        System.out.println("书的静态代码块");
    }

    {
        System.out.println("书的普通代码块");
    }

    Book()
    {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }    public static void staticFunction(){
        System.out.println("书的静态方法");
    }    int price = 110;    static int amount = 112;
}

上面这个例子的输出结果是:

书的普通代码块
书的构造方法
price=110,amount=0
书的静态代码块
书的静态方法

下面我们一步步来分析一下代码的整个执行流程。

在上面两个例子中,因为 main 方法所在类并没有多余的代码,我们都直接忽略了 main 方法所在类的初始化。

但在这个例子中,main 方法所在类有许多代码,我们就并不能直接忽略了。

  • 当 JVM 在准备阶段的时候,便会为类变量分配内存和进行初始化。此时,我们的 book 实例变量被初始化为 null,amount 变量被初始化为 0。
  • 当进入初始化阶段后,因为 Book 方法是程序的入口,根据我们上面说到的类初始化的五种情况的第四种(当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类)。所以JVM 会初始化 Book 类,即执行类构造器 。
  • JVM 对 Book 类进行初始化首先是执行类构造器(按顺序收集类中所有静态代码块和类变量赋值语句就组成了类构造器 ),后执行对象的构造器(按顺序收集成员变量赋值和普通代码块,最后收集对象构造器,最终组成对象构造器 )。

对于 Book 类,其类构造方法()可以简单表示如下:

static Book book = new Book();static{
    System.out.println("书的静态代码块");
}static int amount = 112;

  于是首先执行static Book book = new Book();这一条语句,这条语句又触发了类的实例化。于是 JVM 执行对象构造器 ,收集后的对象构造器 代码:

{
    System.out.println("书的普通代码块");
}int price = 110;
Book()
{
    System.out.println("书的构造方法");
    System.out.println("price=" + price +", amount=" + amount);
}

  于是此时 price 赋予 110 的值,输出:「书的普通代码块」、「书的构造方法」。而此时 price 为 110 的值,而 amount 的赋值语句并未执行,所以只有在准备阶段赋予的零值,所以之后输出「price=110,amount=0」。

  当类实例化完成之后,JVM 继续进行类构造器的初始化:

static Book book = new Book();  //完成类实例化static{
    System.out.println("书的静态代码块");
}static int amount = 112;

即输出:「书的静态代码块」,之后对 amount 赋予 112 的值。

  • 到这里,类的初始化已经完成,JVM 执行 main 方法的内容。
public static void main(String[] args){
    staticFunction();
}

即输出:「书的静态方法」。

方法论

从上面几个例子可以看出,分析一个类的执行顺序大概可以按照如下步骤:

  • 确定类变量的初始值。在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。
  • 初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。
  • 初始化类构造器。JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
  • 初始化对象构造器。JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。

  如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。

  看完了上面的解析之后,再去看看开头那道题是不是觉得简单多了呢。很多东西就是这样,掌握了一定的方法和知识之后,原本困难的东西也变得简单许多了。

  一时没有看懂也不要灰心,毕竟我也是用了不少的时间才弄懂的。不懂的话可以多看几遍,或者加入树义的技术交流群,和小伙们一起交流。

原文地址:https://www.cnblogs.com/xiongbatianxiaskjdskjdksjdskdtuti/p/11356706.html

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer