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

本文详细介绍了JVM的类加载机制,包括加载、验证、准备、解析、初始化、使用和卸载七个阶段。重点讲解了双亲委派模型,解释了其工作原理、优点和缺点,并展示了如何通过自定义类加载器打破这一模型。此外,还探讨了SPI服务发现机制,包括其使用示例、实现原理和常见应用。
摘要由CSDN通过智能技术生成

类的生命周期和加载过程

类的生命周期可以划分为 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 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值