自学内容网 自学内容网

JVM类加载机制,一文搞懂相关细节!

类加载机制

概念

它负责在运行时将Java字节码加载到JVM中,并为之分配内存空间,最终转换成可供JVM执行的操作指令。这一机制不仅关系到Java程序的运行效率和安全性,还直接影响到Java语言的跨平台能力。

加载运行流程

加载:类加载阶段,通过类的全限定名获取二进制字节流,并将其转化为方法区的运行时数据结构,在内存中(堆区)加载字节流生成一个Class对象,在JVM中唯一实体

验证:确保Class文件字节流信息符合JVM要求,并且没有危害,包括检查类文件的格式是否正确、常量池中的符号引用是否有效、访问修饰符是否合法

准备:为类变量分配内存和设置默认值(在方法区分配),这里的默认值是数据类型本身的默认值,int为0,boolean为false,引用类型为null。

解析:将常量池符号引用转化成直接引用。比如类和接口的符号引用解析成类数据在方法区内存布局的指针,从而能够直接引用访问类和接口的信息;字段和方法符号引用解析成实际字段或方法的指针,用于直接访问字段或调用方法

初始化:执行类构造器阶段,由编译器自动收集类中所有类变量的赋值动作和静态代码块,通常这个阶段才是真正的为变量赋值正确的初始值,这是类加载的最后阶段,只有在类被主动使用时才会触发初始化。

触发条件:

  • 创建类的实例时,如使用new关键字创建对象。

  • 访问类的静态方法时。

  • 访问类的静态字段时,并且该字段不是常量字段(被final修饰且在编译期确定值的字段)。

  • 调用java.lang.reflect包中的方法对类进行反射调用时。

  • 初始化子类时,会先触发父类的初始化。

  • 当虚拟机启动时,会先初始化包含main方法的主类。

总之,JVM 的类加载机制是一个复杂而严谨的过程,确保了 Java 程序的安全性、灵活性和动态性。通过类加载机制,JVM 可以在运行时动态地加载、链接和初始化类,实现了 Java 的 “一次编写,到处运行” 的特性。

符号引用替换为直接引用

  • 在 Java 虚拟机中,符号引用是一组符号来描述所引用的目标,它包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。但是这些符号并不能直接指向内存中的实际位置。

    直接引用则是可以直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄等。

    例如,假设有一个类 A ,其中有一个方法 method() 调用了另一个类 B 的方法 bMethod() 。在编译时,对于 B 类及其方法 bMethod() 的引用只是一个符号引用,它可能只是记录了类 B 的全限定名和方法 bMethod() 的名称及描述符。

    但是在运行时,解析阶段会将这个符号引用替换为直接引用。比如,直接引用可能就是内存中类 B 对应方法的实际地址或者偏移量,这样 JVM 就能够直接准确地找到并执行对应的方法。

    再比如,如果引用的是一个字段,符号引用可能只是字段的名称和描述,而直接引用则是指向该字段在内存中的实际存储位置。

    通过将符号引用替换为直接引用,JVM 能够更高效、准确地找到和访问所需要的类、方法或字段,从而保证程序的正常运行。


     

举例理解
一、加载阶段

假设我们有一个名为com.example.MyClass的类,在这个阶段,JVM 会通过类的全限定名去查找并获取这个类的二进制字节流。例如:

  1. 从本地文件系统加载:如果这个类是通过传统的编译方式生成的.class 文件,并且存储在磁盘上的特定路径下,JVM 可以从这个路径读取该类的字节码文件。

    • 例如,在一个 Java 项目中,编译后的类文件通常存储在target/classes目录下,JVM 可以从这个目录中找到com.example.MyClass.class文件并读取其字节码。
  2. 从网络加载:可以通过自定义的类加载器从网络上下载类的字节码。

    • 例如,在一个分布式系统中,某些特定的类可能需要从远程服务器上下载。可以实现一个URLClassLoader的子类,指定远程服务器的 URL,然后通过这个类加载器从网络上加载类的字节码。

加载完成后,JVM 会在内存中(堆区)创建一个代表这个类的java.lang.Class对象。这个Class对象是在 JVM 中表示该类的唯一实体,通过它可以访问该类的各种信息,如方法、字段、构造函数等。

二、验证阶段

  1. 检查类文件的格式是否正确:

    • 例如,验证类文件的魔数是否正确。Java 类文件的开头几个字节是固定的魔数0xCAFEBABE,如果类文件的开头不是这个魔数,说明这个文件不是有效的 Java 类文件,验证不通过。
  2. 检查常量池中的符号引用是否有效:

    • 例如,检查常量池中对其他类、方法或字段的引用是否能够正确解析。如果一个类引用了另一个不存在的类,或者引用了一个不存在的方法或字段,那么在验证阶段会发现这个问题并报告错误。
  3. 检查访问修饰符是否合法:

    • 例如,检查一个类的方法是否被正确地声明为publicprivateprotected或默认访问修饰符。如果一个方法被声明为不合法的访问修饰符组合,验证阶段会发现这个问题并报告错误。

三、准备阶段

假设com.example.MyClass类中有一个静态变量static int count = 0;,在准备阶段,会为这个静态变量分配内存并设置默认值。在这个例子中,count的默认值为 0。

如果有一个引用类型的静态变量,例如static Object reference = null;,在准备阶段,这个引用变量会被初始化为null

四、解析阶段

  1. 类和接口的符号引用解析:

    • 例如,当一个类在代码中引用另一个类时,在编译阶段,这个引用只是一个符号引用,包含了被引用类的全限定名。在解析阶段,JVM 会将这个符号引用解析成类数据在方法区内存布局的指针。
    • 假设com.example.MyClass类中有一个方法调用了另一个类com.example.AnotherClass的静态方法。在编译阶段,这个调用只是一个对com.example.AnotherClass的符号引用。在解析阶段,JVM 会查找并确定com.example.AnotherClass在方法区中的内存布局,并将符号引用转换为直接引用,以便在运行时能够正确地调用该静态方法。
  2. 字段和方法符号引用解析:

    • 例如,当一个类的方法中引用了另一个类的字段或调用了另一个类的方法时,在编译阶段,这些引用都是符号引用。在解析阶段,JVM 会将这些符号引用解析成实际字段或方法的指针。
    • 假设com.example.MyClass类中有一个方法访问了另一个类com.example.AnotherClass的一个实例字段。在编译阶段,这个访问只是一个对com.example.AnotherClass的实例字段的符号引用。在解析阶段,JVM 会查找并确定这个字段在内存中的位置,并将符号引用转换为直接引用,以便在运行时能够正确地访问该字段。

五、初始化阶段

假设com.example.MyClass类中有以下代码:

public class MyClass {
    static int number = 10;
    static {
        System.out.println("Initializing MyClass.");
        // 其他初始化代码
    }
}

在这个例子中,当这个类被主动使用时,比如创建MyClass的实例或者访问MyClass的静态方法或字段,会触发初始化阶段。在初始化阶段,编译器会自动收集类中所有类变量的赋值动作和静态代码块,并按照代码中的出现顺序依次执行。

例如,首先会为静态变量number赋值为 10,然后执行静态代码块中的代码,打印出 "Initializing MyClass."。在这个阶段,才是真正为变量赋值正确的初始值。

备注
编译期

编译期的主要目的之一是得到 Java 字节码。

在编译期,Java 源代码经过一系列的处理步骤,包括词法分析、语法分析、语义分析、中间代码生成、代码优化等过程,最终生成 Java 字节码文件(.class 文件)。

这些字节码是一种平台无关的中间表示形式,它可以在不同的 Java 虚拟机上运行。字节码包含了类的结构信息、方法的指令序列、变量的声明等内容,为 Java 程序的跨平台性提供了基础。

所以,可以说编译期是为了得到 Java 字节码,以便在不同的环境中由 Java 虚拟机执行。

final字段

对于 final 字段,在某些情况下会在编译器进行赋值,在另外一些情况下会在运行时进行赋值。

一、编译时常量的情况

如果 final 字段被声明为编译时常量,即同时满足以下条件:

  1. 被声明为 final。
  2. 使用基本数据类型且被赋予一个常量表达式的值。
  3. 被声明时就进行初始化。

在这种情况下,编译器会将 final 字段的值直接嵌入到使用该字段的地方,相当于在编译期就完成了赋值。

class MyClass {
    final int CONSTANT = 10;
    //...
}

这里的 CONSTANT 是一个编译时常量,编译器会在编译过程中将所有对 CONSTANT 的引用直接替换为 10

二、非编译时常量的情况

如果 final 字段不是编译时常量,例如:

  1. final 字段是引用类型。
  2. final 字段虽然是基本数据类型,但不是在声明时就初始化,而是在构造函数中初始化。

在这种情况下,赋值会在运行时进行,即在对象创建时或者构造函数执行过程中进行赋值。

class MyClass {
    final int nonConstant;
    MyClass(int value) {
        nonConstant = value;
    }
    //...
}

这里的 nonConstant 不是编译时常量,它的值会在创建 MyClass 对象的构造函数中被赋值。


原文地址:https://blog.csdn.net/qq_62097431/article/details/143019827

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!