When will trigger Java class initialization and principles (Detailed)

Copyright: personal essays, problems at work, only to save the document, hoping to help you, please correct me if wrong with progress! Thank you! https://blog.csdn.net/w893932747/article/details/89067482

First Conclusion:

The following cases can trigger an initiating:

  1. Encounter new, getstatic, putstatic, invokestatic which four instructions;
  2. The method of using the package of the class java.lang.reflect reflecting call;
  3. Initializing a class, if it is found not been initialized its parent, the parent class is initialized ( Note that if the interface is its parent, then the parent class initialization is not required! );
  4. When the virtual machine starts, the user needs to specify a main class to be executed (including the main method of the class), the virtual machine to initialize this master class;
  5. When using a dynamic language support jdk1.7 if a method handle java.lang.invoke.MethodHandle instance final analysis result REF_getstatic, REF_putstatic, REF_invokeStatic, and this method handle corresponding to the class not been initialized, the first trigger class initialization thereof;

The following situations will not trigger the kind of initialization:

  1. Similar subclass reference a static field of the parent class, subclass does not cause initialization. As for whether the trigger load and verify subclasses, depending on the specific virtual machine;
public class ClassTest1 {
    public static void main(String[] args) {
        System.out.println(SubClass.num);
//        SubClass subClass=new SubClass();
//        System.out.println(SubClass.num);
    }
}
class SubClass extends SuperClass{
    static {
        System.out.println(111111111);
    }
}
class SuperClass{
    static{
        System.out.println(2222222);
    }
    static int num=100;
}

2. The array definition referenced by the class, class initialization will not be triggered; example: People [] ps = new People [100];

public class ClassTest2 {
    public static void main(String[] args) {
        ArrayClass[] why=new ArrayClass[10];
    }
}

class ArrayClass{
    static {
        System.out.println(1111);
    }
}

3. The constant references to initialize a class will not trigger class

public class ClassTest3 {
    public static void main(String[] args) {
        System.out.println(ConstVariable.TEL);
    }
}
class ConstVariable{
    static {
        System.out.println(1111);
    }
    public static final String TEL="10086";
}

How Java is loaded and initialized

Java virtual machine how to load compiled .class files to the virtual machine inside? How to load initialization after class? Static class initialization process variables and class instance variables are the same, namely, how to initialize it? This article will

It is to solve the three problems above.

If not correct place, a lot of understanding and welcome you to be able to give criticism, thank you in advance.

1. Java virtual machine to load .class process

虚拟机把Class文件加载到内存,然后进行校验解析初始化,最终形成java类型,这就是虚拟机的类加载机制。加载,验证,准备,初始化这5个阶段的顺序是确定的,

类的加载过程,必须按照这种顺序开始。这些阶段通常是相互交叉和混合进行的。解析阶段在某些情况下,可以在初始化阶段之后再开始---为了支持java语言的运行时绑定。

Java虚拟机规范中,没有强制约束什么时候要开始加载,但是,却严格规定了几种情况必须进行初始化(加载,验证,准备则需要在初始化之前开始):

1)遇到 new、getstatic、putstatic、或者invokestatic 这4条字节码指令,如果没有类没有进行过初始化,则触发初始化

2)使用java.lang.reflect包的方法,对垒进行反射调用的时候,如果没有初始化,则先触发初始化

3)初始化一个类时候,如果发现父类没有初始化,则先触发父类的初始化

2. 加载,验证,解析

加载就是通过指定的类全限定名,获取此类的二进制字节流,然后将此二进制字节流转化为方法区的数据结构,在内存中生成一个代表这个类的Class对象。验证是为了确

保Class文件中的字节流符合虚拟机的要求,并且不会危害虚拟机的安全。加载和验证阶段比较容易理解,这里就不再过多的解释。解析阶段比较特殊,解析阶段是虚拟机

将常量池中的符号引用转换为直接引用的过程。如果想明白解析的过程,得先了解一点class文件的一些信息。class文件采用一种类似C语言的结构体的伪结构来存储我们编

码的java类的各种信息。其中,class文件中常量池(constant_pool)是一个类似表格的仓库,里面存储了我们编写的java类的类和接口的全限定名,字段的名称和描述符,

方法的名称和描述符。在java虚拟机将class文件加载到虚拟机内存之后,class类文件中的常量池信息以及其他的数据会被保存到java虚拟机内存的方法区。我们知道class文件

的常量池存放的是java类的全名,接口的全名和字段名称描述符,方法的名称和描述符等信息,这些数据加载到jvm内存的方法区之后,被称做是符号引用。而把这些类的

全限定名,方法描述符等转化为jvm可以直接获取的jvm内存地址,指针等的过程,就是解析。虚拟机实现可以对第一次的解析结果进行缓存,避免解析动作的重复执行。

在解析类的全限定名的时候,假设当前所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或者接口C的直接引用,具体的执行办法就是虚拟机会把代表N的

全限定名传递给D的类加载器去加载这个类C。这块可能不太好理解,但是我们可以直接理解为调用D类的ClassLoader来加载N,然后就完成了N--->C的解析,就可以了。

3. 准备阶段

之所以把在解析阶段前面的准备阶段,拿到解析阶段之后讲,是因为,准备阶段已经涉及到了类数据的初始化赋值。和我们本文讲的初始化有关系,所以,就拿到这里来讲

述。在java虚拟机加载class文件并且验证完毕之后,就会正式给类变量分配内存并设置类变量的初始值。这些变量所使用的内存都将在方法区分配。注意这里说的是类变量,

也就是static修饰符修饰的变量,在此时已经开始做内存分配,同时也设置了初始值。比如在 Public static int value = 123 这句话中,在执行准备阶段的时候,会给value

分配内存并设置初始值0, 而不是我们想象中的123. 那么什么时候 才会将我们写的123 赋值给 value呢?就是我们下面要讲的初始化阶段。

4. 初始化阶段

类初始化阶段是类加载过程的最后阶段。在这个阶段,java虚拟机才真正开始执行类定义中的java程序代码。Java虚拟机是怎么完成初始化的呢?这要从编译开始讲起。在编

译的时候,编译器会自动收集类中的所有静态变量(类变量)和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是根据语句在java代码中的顺序决定的。

收集完成之后,会编译成java类的 static{} 方法,java虚拟机则会保证一个类的static{} 方法在多线程或者单线程环境中正确的执行,并且只执行一次。在执行的过程中,便完

成了类变量的初始化。值得说明的是,如果我们的java类中,没有显式声明static{}块,如果类中有静态变量,编译器会默认给我们生成一个static{}方法。 我们可以通过

javap -c 的命令,来看一下java字节码中编译器为我们生成或者合并的static{} 方法:

public class StaticValInitTest {

    public static int value = 123;

}

上面我们讲述的是单类的情况,如果出现继承呢?如果有继承的话,父类中的类变量该如何初始化?这点由虚拟机来解决:虚拟机会保证在子类的static{}方法执行之

前,父类的static{}方法已经执行完毕。由于父类的static{}方法先执行,也就意味着父类的静态变量要优先于子类的静态变量赋值操作。

上面讲的都是静态变量,实例变量怎么解决呢?实例变量的初始化,其实是和静态变量的过程是类似的,但是时间和地点都不同哦。我们以下面的Dog类为例来讲一讲。

public class Dog {

    public String type = "tai di";

    public int age = 3;
}

1)当用new Dog() 创建对象的时候,首先在堆上为Dog对象分配足够的空间。

2)这块存储空间会被清零,这就是自动将Dog对象中的所有基本类型的数据都设置成了默认值,而引用类型则被设置成了null(类似静态类的准备阶段的过程)

3)Java收集我们的实例变量赋值语句,合并后在构造函数中执行赋值语句。没有构造函数的,系统会默认给我们生成构造函数。

至此,java类初始化的理论基础已经完成了,其中的大部分的理论和思想都出自《深入理解java虚拟机》这本书。有了以上的理论基础,

再复杂的类初始化的情况,我们都可以应对了,下面就拿一个例子做一个具体的分析吧

public class Insect {
    private int i = 9;
    protected int j;
    
    protected static int x1 = printInit("static Insect.x1 initialized");
    
    Insect() {
        System.out.println("基类构造函数阶段: i = " + i + ", j = " + j);
        j = 39;
    }
    
    static int printInit(String s) {
        System.out.println(s);
        return 47;
    }
}

public class Beetle extends Insect {
    
    protected int k = printInit("Beetle.k initialized");
    
    protected static int x2 = printInit("static Beetle.x2 initialized");    
    
    public static void main(String[] args) {        
        Beetle b = new Beetle();
    }
}

上面例子来自《java编程思想》,以上代码的执行结果是什么呢?如果对上面我们讲的理论理解的话,很容易就知道结果是:

static Insect.x1 initialized

static Beetle.x2 initialized

基类构造函数阶段: i = 9, j = 0

Beetle.k initialized

具体的执行结果过程是:

在执行Beetle 类的 main方法的时候,因为该main方法是static方法,我们在上面已经知道,在执行类的static方法的时候,如果该类没有初始化,则要进行初始化,

因此,我们在执行main方法的时候,会执行加载--验证--准备--解析---初始化这个过程。在进行最后的初始化的时候,又有一个约束:虚拟机会保证在子类的static{}

方法执行之前,父类的static{}方法已经执行完毕。所以,在执行完解析之后,会先执行父类的初始化,在执行父类初始化的时候,

输出: static Insect.x1 initialized

然后接着初始化子类,输出:static Beetle.x2 initialized

以上两行输出,是静态变量的初始化,是在第一次调用静态方法,即,在执行new、getstatic、putstatic、或者invokestatic 这4条字节码指令时候触发的。所以,

你如果把上例中的static main 方法中的 Beetle b = new Beetle(); 

注释掉,上面两行仍然会输出出来。然后就是执行Beetle b = new Beetle();这句代码了。我们知道,在实例化子类对象的时候,会自动调用父类的构造函数。

所以,接着就输出:基类构造函数阶段: i = 9, j = 0

紧接着是执行自己的构造函数,在堆上创建类实例对象,实例对象空间清零,然后执行赋值语句k = printInit("Beetle.k initialized");

输出: Beetle.k initialized

至此,整个类加载并初始化完毕,是不是理解起来就很简单了,趁胜追击,我们还是再来看一个例子吧:

public class Base {

    Base() {
        preProcess();
    }

    void preProcess() {
    }
}

public class Derived extends Base {

    public String whenAmISet = "set when declared"; 

    @Override 
    void preProcess() {

        whenAmISet = "set in preProcess";

    }    

    public static void main(String[] args) {
        Derived d = new Derived();
        System.out.println(d.whenAmISet);
    }
}

一个地方比较绕:父类在执行构造函数的时候,调用了子类(导出类)重载过的方法,在子类的重载方法中,给实例变量做了一次赋值,正是这次赋值,干扰了我们对类初始化的理解。

我们不管类里面是怎么做的,还按照我们上个例子中那样进行分析:

1. 执行Derived 类 static main 方法的时候,执行类变量初始化,但是此例中父类和子类都没有类变量,所以此步骤什么都不做,进行实例变量初始化

2. 执行new Derived()的时候,先调用了父类的构造函数,因为子类的重载,调用了子类的preProcess方法,为实例变量whenAmISet 赋值为"set in preProcess"

3. 然后执行子类Derived 的构造函数,在构造函数中,有编译器为我们收集生成的实例变量赋值语句,最终,又将实例变量whenAmISet 赋值为"set when declared"

4. 所以最终的输出是: set when declared

如果对这个还不太理解的话,可以再Derived 类里面添加注释,改成下面的样子,输出看看,是不是对这个执行过程更清晰了呢?

public class Derived extends Base {
    
    // 准备阶段赋值 whenAmISet=null
    public String whenAmISet = "set when declared";
    
    public Derived() {
        System.out.println("do son constructor");
    }
     
    @Override 
    void preProcess() {
        System.out.println("do son process");
        System.out.println("whenAmISet:" + whenAmISet);
        whenAmISet = "set in preProcess";
        System.out.println("whenAmISet:" + whenAmISet);
        System.out.println("set in preProcess end");
    }

    public static void main(String[] args) {
        Derived d = new Derived();
        System.out.println(d.whenAmISet);
    }
}    

参考文献:

http://www.cnblogs.com/jimxz/p/3974939.html

https://www.cnblogs.com/hujinshui/p/10422521.html

Guess you like

Origin blog.csdn.net/w893932747/article/details/89067482