Heim > Java > javaLernprogramm > Detaillierte Einführung in den Java-Klassenlademechanismus

Detaillierte Einführung in den Java-Klassenlademechanismus

angryTom
Freigeben: 2019-08-15 16:32:12
nach vorne
2276 Leute haben es durchsucht

Detaillierte Einführung in den Java-Klassenlademechanismus

Artikel nachgedruckt von:http://www.pythonheidong.com/blog/article/1152/

In vielen Java Bei Interviews sehen wir oft Inspektionen zum Java-Klassenlademechanismus, wie zum Beispiel die folgende Frage:

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);  //入口
    }
}
Nach dem Login kopieren
Nach dem Login kopieren

Bitte schreiben Sie die endgültige Ausgabezeichenfolge.

Die richtige Antwort lautet:

爷爷在静态代码块
爸爸在静态代码块
爸爸的岁数:25
Nach dem Login kopieren
Nach dem Login kopieren

Ich glaube, dass nach dem Anblick dieser Frage bei vielen Schülern die Miene zusammenbrach und sie keine Ahnung hatten, wo sie anfangen sollten. Manche sind sogar mehrmals darauf gestoßen und konnten immer noch nicht die richtige Lösung finden.

Tatsächlich testet diese Art von Interviewfrage Ihr Verständnis des Java-Klassenlademechanismus.

Wenn Sie den Java-Lademechanismus nicht verstehen, können Sie diese Frage nicht beantworten.

In diesem Artikel werde ich Ihnen zunächst die Grundkenntnisse des Ladens von Java-Klassen vermitteln und dann einige Fragen in der Praxis analysieren, um Ihnen das Verständnis der Ideen zu erleichtern.

Lernen wir zunächst die sieben Stufen des Java-Klassenlademechanismus kennen.

Empfohlenes Tutorial: „Java-Video-Tutorial

Seven Java Class Loading Mechanisms Stage

Wenn unser Java-Code kompiliert wird, wird die entsprechende Klassendatei generiert. Wenn wir dann den Befehl java Demo ausführen, starten wir tatsächlich die virtuelle JVM-Maschine, um den Inhalt der Klassenbytecodedatei auszuführen. Der Prozess der Ausführung von Klassenbytecode durch eine virtuelle JVM-Maschine kann in sieben Phasen unterteilt werden: Laden, Verifizieren, Vorbereiten, Parsen, Initialisierung, Verwendung und Deinstallation.

Laden

Das Folgende ist die offiziellste Beschreibung des Ladevorgangs.

Die Ladephase ist die erste Phase des Klassenladevorgangs. In dieser Phase besteht der Hauptzweck der JVM darin, den Bytecode von verschiedenen Orten (Netzwerk, Festplatte usw.) in einen binären Bytestrom umzuwandeln und ihn in den Speicher zu laden. Anschließend wird ein entsprechendes Klassenobjekt für diese Klasse erstellt Der Methodenbereich der JVM ist der Zugriffseingang auf verschiedene Daten dieser Klasse.

Tatsächlich lässt sich die Ladephase in einem Satz zusammenfassen: Laden von Codedaten in den Speicher. Dieser Prozess steht nicht in direktem Zusammenhang mit unserer Antwort auf diese Frage, sondern ist ein Prozess des Klassenlademechanismus und muss daher erwähnt werden.

Überprüfung

Nachdem die JVM die Klassenbytecode-Datei geladen und das entsprechende Klassenobjekt im Methodenbereich erstellt hat, beginnt die JVM mit der Überprüfung des Bytecode-Streams. Nur die JVM führt nur Dateien mit Bytecode durch Spezifikationen können von der JVM korrekt ausgeführt werden. Dieser Überprüfungsprozess kann grob in die folgenden Typen unterteilt werden:

  • JVM-Spezifikationsüberprüfung. JVM führt eine Dateiformatüberprüfung des Bytestreams durch, um festzustellen, ob er den JVM-Spezifikationen entspricht und ob er von der aktuellen Version der virtuellen Maschine verarbeitet werden kann. Zum Beispiel: ob die Datei mit 0x cafe bene beginnt, ob die Haupt- und Nebenversionsnummern im Verarbeitungsbereich der aktuellen virtuellen Maschine liegen usw.
  • Überprüfung der Codelogik. Die JVM überprüft den Datenfluss und den Kontrollfluss, der aus dem Code besteht, um sicherzustellen, dass keine schwerwiegenden Fehler auftreten, nachdem die JVM die Bytecode-Datei ausgeführt hat. Beispielsweise erfordert eine Methode die Übergabe eines Parameters vom Typ int, bei ihrer Verwendung wird jedoch ein Parameter vom Typ String übergeben. Eine Methode hat die Rückgabe eines Ergebnisses vom Typ String angefordert, aber am Ende wurde kein Ergebnis zurückgegeben. Der Code verweist auf eine Klasse namens Apple, aber Sie definieren die Apple-Klasse nicht wirklich.

Wenn die Codedaten in den Speicher geladen werden, überprüft die virtuelle Maschine die Codedaten, um festzustellen, ob der Code tatsächlich gemäß den JVM-Spezifikationen geschrieben wurde. Dieser Prozess steht nicht in direktem Zusammenhang mit unserer Antwort auf die Frage, es ist jedoch erforderlich, diesen Prozess zu kennen, um den Klassenlademechanismus zu verstehen.

Vorbereitung (Wichtige Punkte)

Nach Abschluss der Überprüfung der Bytecode-Datei beginnt die JVM, Speicher für Klassenvariablen zuzuweisen und zu initialisieren. Hier müssen zwei wichtige Punkte beachtet werden, nämlich das zugewiesene Speicherobjekt und die Art der Initialisierung.

  • Speicherzugeordnetes Objekt. In Java gibt es zwei Arten von Variablen: „Klassenvariablen“ und „Klassenmitgliedsvariablen“. „Klassenvariablen“ beziehen sich auf statisch geänderte Variablen, während alle anderen Arten von Variablen zu „Klassenmitgliedsvariablen“ gehören. Während der Vorbereitungsphase reserviert die JVM nur Speicher für „Klassenvariablen“ und nicht für „Klassenmitgliedsvariablen“. Die Speicherzuweisung von „Klassenmitgliedsvariablen“ muss bis zur Initialisierungsphase warten.

Zum Beispiel wird in der Vorbereitungsphase des folgenden Codes Speicher nur für das Faktorattribut reserviert, nicht jedoch für das Website-Attribut.

public static int factor = 3;public String website = "www.cnblogs.com/chanshuyi";
Nach dem Login kopieren
  • Der initialisierte Typ. Während der Vorbereitungsphase weist die JVM Speicher für Klassenvariablen zu und initialisiert diese. Unter Initialisierung versteht man hier jedoch die Zuweisung des Nullwerts des Datentyps in der Java-Sprache zur Variablen, nicht des im Benutzercode initialisierten Werts.

Beispielsweise ist im folgenden Code nach der Vorbereitungsphase der Wert des Sektors 0 statt 3.

public static int sector = 3;
Nach dem Login kopieren

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

public static final int number = 3;
Nach dem Login kopieren

之所以 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;
}
Nach dem Login kopieren

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

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

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

书的静态代码块
Hello ShuYi.
Nach dem Login kopieren

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

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

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

重点来了!

重点来了!

重点来了!

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

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

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

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

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

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

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

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

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

因为我们确实没有进行 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);  //入口
    }
}
Nach dem Login kopieren
Nach dem Login kopieren

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

最终的输出结果是:

爷爷在静态代码块
爸爸在静态代码块
爸爸的岁数:25
Nach dem Login kopieren
Nach dem Login kopieren

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

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

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

  • 首先程序到 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();  //入口
    }
}
Nach dem Login kopieren

输出结果是:

爷爷在静态代码块
爸爸在静态代码块
儿子在静态代码块
我是爷爷~
我是爸爸~
我是儿子~
Nach dem Login kopieren

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

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

  • 首先在入口这里我们实例化一个 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;
}
Nach dem Login kopieren

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

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

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

在上面两个例子中,因为 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;
Nach dem Login kopieren

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

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

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

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

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

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

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

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

方法论

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

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

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

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

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

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

Das obige ist der detaillierte Inhalt vonDetaillierte Einführung in den Java-Klassenlademechanismus. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Verwandte Etiketten:
Quelle:cnblogs.com
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage