对类加载时机中初始化(Initialization)时机的理解

1. 类的生命周期过程

从类被加载到JVM内存中开始到被卸载出内存为止,生命周期会经过以下7过程:
在这里插入图片描述

  • 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。
  • 解析阶段不一定按上图顺序进行,在某些情况下解析阶段可以在初始化之后进行(为了支持Java的运行时绑定特性,也称为动态绑定)

2. 六种必须立即进行初始化的操作

”加载“过程什么时候开始并没有明确的规定,但是什么时候必须进行类初始化有着严格的规定,仅在以下6种情况中需要立即进行类初始化操作(初始化之前的必要操作加载、验证、准备肯定也要进行)
这6种场景称为对一个类型进行“主动引用”,除此之外所有引用类型方式都不会触发初始化

  1. 遇到new、getstatic、putstatic或者invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
  • 使用new关键字实例化对象的时候
  • 读取或者设置一个类型的静态字段时(被final修饰,已在编译期将结果放入常量池的静态字段除外,注意是除外除外除外!)
  • 调用一个类型的静态方法的时候
  1. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果没有进行过初始化,则需要先进行初始化
  2. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  3. 当JVM启动的时候,用户需要指定一个要执行的主类(包含main方法的类)虚拟机会先初始化这个类
  4. 当使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getstatic、REF_putstatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
  5. 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,则该接口要在其之前被初始化

3. 不会触发 类初始化 的情况

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

列举三种不会触发类初始化的场景:

  • Demo1: 通过子类引用父类的静态字段,不会导致子类初始化
class A{
    public static String super_str = "super_str";

    static {
        System.out.println("A初始化...");
    }
    A(){
        System.out.println("A被创建...");
    }
}

class B extends A{
    public static String child_str = "child_str";

    static {
        System.out.println("B初始化...");
    }
    B(){
        System.out.println("B被创建...");
    }
}

public class Demo1 {
    public static void main(String[] args) {
        // 通过子类引用父类中的静态字段
        System.out.println(B.super_str);
    }
}

输出结果及其分析:

A初始化...
super_str

分析:对于静态字段,只有直接定义这个字段的类才会被初始化,
因此通过其子类来引用父类中定义的静态字只会触发父类的初始化

除此之外:是否要触发子类的加载和验证阶段:
在HotSpot VM通过-XX:+TraceClassLoading参数观察到此操作是 会触发的
  • Demo2: 使用数组定义来引用类,不会触发类的初始化
class A{
    static {
        System.out.println("A初始化...");
    }
}
public class Demo2 {
    public static void main(String[] args) {
        // 使用数组定义来引用类,不会触发类的初始化
        A[] aa = new A[10];
    }
}

输出结果及其分析:

什么也没输出...

分析:没有任何输出说明没有触发A类的初始化

(以下内容暂未实验,暂时转述书本上的内容)

但是会触发一个名为"[Lorg.feinxsoft.classloading.A"的类初始化阶段,对于用户代码来说,
这不是一个合法的类型名称,它是一个由JVM自动生成的,直接继承与java.lang.Object的子类,
创建动作由字节码指令newarray触发。

这个类代表了一个元素类型为org.feinxsoft.classloading.A的一维数组,
数组中应有的属性和方法都被实现在这个类里面。
  • Demo3: 使用类中的final常量,不会触发类的初始化
class A{

	// 和Demo1相比仅仅多了final
    public static final String super_str = "super_str";

    static {
        System.out.println("A初始化...");
    }
    A(){
        System.out.println("A被创建...");
    }
}
public class Demo3 {
    public static void main(String[] args) {
        // // 使用类中的final常量
        System.out.println(A.super_str);
    }
}

输出结果及其分析:

super_str

分析:常量在编译阶段会被存入调用类的常量值中,本质上没有直接引用到定义常量的类,
因此不会触发定义常量的类的初始化。

在编译阶段通过常量传播优化,已经将常量的值"super_str"直接存储在Demo3的类的常量池中,
以后Demo3中对常量super_str的引用实际上都被转化为Demo3类对自身常量池的引用了。

实际上Demo3的Class文件中并没有A类的符号引用入口,
这两个类在编译成Class文件之后就没有任何联系了

4. 有关接口

接口也有初始化过程,在前面的6中必须进行初始化的操作中,有一点不同:

  • 当一个类在初始化的时候,要求其父类全部已经初始化过了,否则先对父类进行初始化
  • 当一个接口在进行初始化的时候,不要求其父类接口全部完成了初始化,只有在真正使用到父接口的时候才会初始化,比如要使用父接口中定义的常量的时候。

参考:
https://www.jianshu.com/p/7ecbb90478ef
https://blog.csdn.net/weixin_34211761/article/details/88030782

原创文章 187 获赞 29 访问量 6万+

猜你喜欢

转载自blog.csdn.net/weixin_43826242/article/details/105407428