Home >Java >javaTutorial >Detailed introduction to java class loading mechanism

Detailed introduction to java class loading mechanism

angryTom
angryTomforward
2019-08-15 16:32:122251browse

Detailed introduction to java class loading mechanism

The article is reproduced from:http://www.pythonheidong.com/blog/article/1152/

In many Java During interviews, we often see inspections about the Java class loading mechanism, such as the following question:

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);  //入口
    }
}

Please write the final output string.

The correct answer is:

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

I believe that many students’ expressions collapsed after seeing this question, and they had no idea where to start. Some even encountered it several times and still couldn't find the correct solution.

In fact, this kind of interview question tests your understanding of the Java class loading mechanism.

If you don’t understand the Java loading mechanism, then you will not be able to answer this question.

So in this article, I will first take you to learn the basic knowledge of Java class loading, and then analyze a few questions in practice to help you grasp the ideas.

Let’s first learn the seven stages of the Java class loading mechanism.

Recommended Tutorial: "java Video Tutorial"

Seven Java Class Loading Mechanisms Phase

When our Java code is compiled, the corresponding class file will be generated. Then when we run the java Demo command, we actually start the JVM virtual machine to execute the contents of the class bytecode file. The process of JVM virtual machine executing class bytecode can be divided into seven stages: Loading, verification, preparation, parsing, initialization, use, and uninstallation.

Loading

The following is the most official description of the loading process.

The loading phase is the first stage of the class loading process. At this stage, the main purpose of the JVM is to convert the bytecode from various locations (network, disk, etc.) into a binary byte stream and load it into memory. Then, a corresponding Class object will be created for this class in the method area of ​​the JVM. This Class object is the access entrance to various data of this class.

In fact, the loading phase can be said in one sentence: loading code data into memory. This process is not directly related to our answer to this question, but it is a process of the class loading mechanism, so it must be mentioned.

Verification

When the JVM loads the Class bytecode file and creates the corresponding Class object in the method area, the JVM will start verification of the bytecode stream. Only the JVM Only files with bytecode specifications can be executed correctly by the JVM. This verification process can be roughly divided into the following types:

  • JVM specification verification. JVM will perform file format verification on the byte stream to determine whether it complies with JVM specifications and whether it can be processed by the current version of the virtual machine. For example: whether the file starts with 0x cafe bene, whether the major and minor version numbers are within the processing range of the current virtual machine, etc.
  • Code logic verification. The JVM will verify the data flow and control flow composed of the code to ensure that no fatal errors will occur after the JVM runs the bytecode file. For example, a method requires a parameter of type int to be passed in, but when using it, a parameter of type String is passed in. A method asked to return a result of type String, but in the end no result was returned. The code references a class called Apple, but you don't actually define the Apple class.

When the code data is loaded into the memory, the virtual machine will verify the code data to see if the code is actually written in accordance with the JVM specifications. This process is not directly related to our answer to the question, but it is necessary to know this process to understand the class loading mechanism.

Preparation (Key Points)

After completing the verification of the bytecode file, the JVM will begin to allocate memory for class variables and initialize it. Two key points need to be noted here, namely the memory allocated object and the type of initialization.

  • Memory allocated object. There are two types of variables in Java: "class variables" and "class member variables". "Class variables" refer to variables modified by static, while all other types of variables belong to "class member variables". During the preparation phase, the JVM will only allocate memory for "class variables" and not for "class member variables". The memory allocation of "class member variables" needs to wait until the initialization phase.

For example, in the preparation phase of the following code, memory will only be allocated for the factor attribute, but not for the website attribute.

public static int factor = 3;public String website = "www.cnblogs.com/chanshuyi";
  • Initialized type. In the preparation phase, the JVM will allocate memory for class variables and initialize them. But initialization here refers to assigning the zero value of the data type in the Java language to the variable, not the value initialized in the user code.

For example, in the following code, after the preparation phase, the value of sector will be 0 instead of 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

The above is the detailed content of Detailed introduction to java class loading mechanism. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:cnblogs.com. If there is any infringement, please contact admin@php.cn delete