类加载过程整理

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/heroqiang/article/details/86638766

各种调用初始化顺序

我们用一个小demo来验证一下各种调用的初始化顺序:

demo1
public class Test {

    public static void main(String[] args) {
        new C1();
    }

}

class C1 {

    public static C2 c2 = new C2();

    public C3 c3 = new C3();

    static {
        System.out.println("C1 static block execute");
    }

    public C1() {
        System.out.println("C1 constructor execute");
    }

    {
        System.out.println("C1 free block execute");
    }
}

class C2 {

    static {
        System.out.println("C2 static block execute");
    }

    {
        System.out.println("C2 free block execute");
    }

    public C2() {
        System.out.println("C2 constructor execute");
    }
}

class C3 {

    static {
        System.out.println("C3 static block execute");
    }

    {
        System.out.println("C3 free block execute");
    }

    public C3() {
        System.out.println("C3 constructor execute");
    }
}
结果输出1
C1 static block execute
C2 static block execute
C2 free block execute
C2 constructor execute
C3 static block execute
C3 free block execute
C3 constructor execute
C1 free block execute
C1 constructor execute
demo2

我们交换类C1中C2类型的静态变量,放到静态块下面:

class C1 {

    public C3 c3 = new C3();

    static {
        System.out.println("C1 static block execute");
    }

    public static C2 c2 = new C2();

    public C1() {
        System.out.println("C1 constructor execute");
    }

    {
        System.out.println("C1 free block execute");
    }
}
结果输出2
C1 static block execute
C2 static block execute
C2 free block execute
C2 constructor execute
C3 static block execute
C3 free block execute
C3 constructor execute
C1 free block execute
C1 constructor execute
demo3

接下来我们交换类C1中C3类型的实例属性,放到整个类的最下面

class C1 {

    static {
        System.out.println("C1 static block execute");
    }

    public static C2 c2 = new C2();

    public C1() {
        System.out.println("C1 constructor execute");
    }

    {
        System.out.println("C1 free block execute");
    }

    public C3 c3 = new C3();

}
结果输出3
C1 static block execute
C2 static block execute
C2 free block execute
C2 constructor execute
C1 free block execute
C3 static block execute
C3 free block execute
C3 constructor execute
C1 constructor execute
demo4

在C1中添加静态方法,main方法中的调用换成调用C1的静态方法:

public class Test {

    public static void main(String[] args) {
        C1.staticInvoke();
    }

}

class C1 {

    static {
        System.out.println("C1 static block execute");
    }

    public static C2 c2 = new C2();

    public C1() {
        System.out.println("C1 constructor execute");
    }

    {
        System.out.println("C1 free block execute");
    }

    public C3 c3 = new C3();

    public static void staticInvoke() {
        System.out.println("C1 static method invoke");
    }

}
结果输出4
C1 static block execute
C2 static block execute
C2 free block execute
C2 constructor execute
C1 static method invoke
结论

通过以上几个例子我们可以总结出以下结论(未完待续):

  • 静态属性(包括静态变量、静态块)的初始化优先于实例属性
  • 游离块的初始化优先于构造方法
  • 同为静态属性,代码中定义的顺序决定了初始化顺序
  • 同为实例属性,代码中定义的顺序决定了初始化顺序
  • 只调用类的静态方法,不会初始化类的实例属性

类加载过程

下图为类从被加载到到虚拟机内存开始,到卸载出内存为止,生命周期的七个阶段:
在这里插入图片描述
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定,它在某些情况下可以再初始化阶段之后再“开始”,这是为了支持java语言的运行时绑定,这里强调按部就班地“开始”,而不是按部就班地“进行”或者“完成”,是因为这些阶段通常都是互相交叉混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。

什么情况下需要开始类加载过程的第一个阶段:“加载”。java虚拟机规范中并没强行约束,这点可以交给虚拟机的的具体实现自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种必须对类进行“初始化”(则加载、验证、准备自然需要在此之前开始):

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

以上5种触发类的初始化的场景称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用

  1. 通过子类引用父类的静态字段,不会导致子类初始化。
  2. 通过数组定义类应用类,不会触发此类的初始化。
  3. 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

接下来我们来看一下类加载的每个过程都做了那些事:

加载:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

验证:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

准备:

  • 为类变量分配内存并设置类的变量的初始值

这里说的初始值也就是变量的默认值(零值),如int型的默认值是0,boolean型的默认值是false,引用类型的默认值为null等。

解析:

  • 类或接口的解析
  • 字段解析
  • 类方法解析
  • 接口方法解析

初始化:

  • 执行类构造器<clinit>()方法的过程

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。

<clinit>()方法

<clinit>()方法是在类加载的初始化阶段执行的类构造器,我们要把<clinit>()方法和构造函数(实例的构造器<init>()方法)区分开来,<clinit>()方法并不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

接口中不能使用静态语句块,但仍然有变量的初始化操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

猜你喜欢

转载自blog.csdn.net/heroqiang/article/details/86638766
今日推荐