• JVM类加载机制、双亲委派和SPI机制


    类的生命周期和加载过程

    类的生命周期可以划分为 7 个阶段

    1. 加载
    2. 验证
    3. 准备
    4. 解析
    5. 初始化
    6. 使用
    7. 卸载

    其中,第 1~5 阶段,即加载、验证、准备、解析、初始化,统称为「类加载」,如下图所示。

    1.加载

    加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。

    该过程可以总结为「JVM 加载 Class 字节码文件到内存中,并在方法区创建对应的 Class 对象」。

    2.验证

    当 JVM 加载完 Class 字节码文件,并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。

    这个校验过程,大致可以分为下面几个类型

    1. JVM 规范校验
      0x cafe babe
      
    2. 代码逻辑校验
      • JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。
      • 例如,一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。

    3.准备

    准备阶段中,JVM 将为类变量分配内存并初始化。

    准备阶段,有两个关键点需要注意

    1. 内存分配的对象
    2. 初始化的类型

    内存分配的对象

    Java 中的变量有「类变量」和「类成员变量」两种类型。「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。 在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。

    public static int factor = 3;
    public String website = "www.google.com";
    复制代码

    如上代码,在准备阶段,只会为 factor 变量分配内存,而不会为 website 变量分配内存。

    初始化的类型

    在准备阶段,JVM 会为「类变量」分配内存并为其初始化。这里的「初始化」指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。

    public static int sector = 3;
    复制代码

    如上代码,在准备阶段后, sector 的值将是 0,而不是 3。

    如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,变量便会被赋予用户希望的值。 final 关键字用在变量上,表示该变量不可变,一旦赋值就无法改变。所以,在准备阶段中,对类变量初始化赋值时,会直接赋予用户希望的值。

    public static final int number = 3;
    复制代码

    如上代码,在准备阶段后, number 的值将是 3,而不是 0。

    4.解析

    解析过程中,JVM 针对「类或接口」、「字段」、「类方法」、「接口方法」、「方法类型」、「方法句柄」、「调用点限定符」这 7 类引用进行解析。解析过程的主要任务是将其在常量池中的符号引用,替换成其在内存中的直接引用。

    5.初始化

    到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化。

    一般来说,当 JVM 遇到下面 5 种情况的时候会触发初始化

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

    6.使用

    当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。

    7.卸载

    当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。

    对类加载的理解

    下面,将通过几个案例,对类加载的 5 个阶段加深理解。

    类初始化方法和对象初始化方法

    public class Book {
        public static void main(String[] args) {
            System.out.println("Hello Liu Baoshuai");
        }
    
        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;
    }
    复制代码

    运行上述代码,输出信息如下。

    书的静态代码块
    Hello Liu Baoshuai
    复制代码

    下面对输出结果进行分析。

    根据「类的生命周期和加载过程 / 5.初始化」章节中提到的「当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类」可知,我们将会进行类的初始化。

    Java 源代码中有构造方法这个概念。但编译为字节码后,是没有构造方法这个概念的,只有「类初始化方法」和「对象初始化方法」。

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

    上面的例子中,其类初始化方法如下。

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

    上面的例子中,其对象初始化方法如下。

    {
            System.out.println("书的普通代码块");
        }
    
        int price = 110;
    
        //注意,构造函数的代码一定是被放在最后的
        Book() {
            System.out.println("书的构造方法");
            System.out.println("price=" + price +",amount=" + amount);
        }
    复制代码

    结合「类初始化方法」和「对象初始化方法」的分析,再回过头看上述例子,就不难得出结论了。

    1. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。所以开始执行「初始化」过程。
    2. main 方法中,并没有实例化对象,所以只执行「类初始化方法」,如下所示。因此,会输出 书的静态代码块 。
    static {
            System.out.println("书的静态代码块");
        }
    
        static int amount = 112;
    复制代码
    1. 初始化过程执行完毕后,继续执行 main() 方法。因此,会输出 Hello Liu Baoshuai 。

    案例引申

    下面,对上述测试案例进一步引申,修改 main() 方法,代码如下所示。

    public class Book {
        public static void main(String[] args) {
            System.out.println("Hello Liu Baoshuai" + new Book().price);
        }
    
        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;
    }
    复制代码

    运行上述代码,输出信息如下。

    书的静态代码块
    书的普通代码块
    书的构造方法
    price=110,amount=112
    Hello Liu Baoshuai110
    复制代码

    下面对输出结果进行分析。

    书的静态代码块
    
    static {
            System.out.println("书的静态代码块");
        }
    
        static int amount = 112;
    复制代码
    1. 「类初始化方法」执行完毕后,继续执行 main() 方法。遇到了 new Book() 语句,所以触发执行「对象初始化方法」,如下所示。
    // part 1
        {
            System.out.println("书的普通代码块");
        }
        // part 2
        int price = 110;
    
        // part 3
        Book() {
            System.out.println("书的构造方法");
            System.out.println("price=" + price +",amount=" + amount);
        }
    复制代码
    1. 需要注意的是, part 1 和 part 2 的先后顺序,是根据它们在代码中出现的顺序决定的。 part 3 部分是构造函数部分,这部分永远是出现最后的,和它在代码中的顺序无关。在代码中, part 3 部分虽然出现在 part 1 和 part 2 的前面,但在「对象初始化方法」中,它永远是出现在最后的。
    2. 此外,由于 part 2 出现在 part 3 前面,所以输出 price 的值是 110,而不是 0。

    继承关系下类的加载情况

    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
    复制代码

    下面对输出结果进行分析。

    1. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。所以开始执行「初始化」过程。
    2. main 方法中,并没有实例化对象,所以只执行「类初始化方法」,不会执行「对象初始化方法」。
    3. 根据「类的生命周期和加载过程 / 5.初始化」章节中提到的「当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化」可知,进行 Son 初始化时,会先进行父类 Father 的初始化。同理,进行 Father 初始化时,会先进行父类 Grandpa 的初始化。所以,程序会输出如下信息。
    爷爷在静态代码块
    爸爸在静态代码块
    复制代码
    1. 继续,执行 main() 方法中的 System.out.println 语句,程序会输出 爸爸的岁数: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(); 	//入口
  • 相关阅读:
    使用 Hibernate Envers 进行实体审计
    C++ Reference: Standard C++ Library reference: Others: iterator: end
    Spring数据绑定之DataBinder篇---01
    “蔚来杯“2022牛客暑期多校训练营10 F.Shannon Switching Game?
    农民工学CSAPP题目解析-前篇题目解答以及答疑总结
    “萝卜快跑”仍然存在出现多处高危网络安全问题
    Spring之aop
    Windows如何将软件安装在移动硬盘上?
    Python按照拼音顺序给数组排序
    flutter生态一统甜夏 @Android @ios @windowse @macos @linux @Web
  • 原文地址:https://blog.csdn.net/mfmfmfo/article/details/126870252