✅Java中类加载的过程是怎么样的?

典型回答

Java中类的加载阶段分为加载(Loading)、链接(Linking)和初始化(Initialization)。其中连接过程又包含了验证、准备和解析。

1706422325589-7a4b6e5c-360f-4137-82c8-f43eacda5c4f.png

加载阶段

加载阶段的目的是将类的.class文件加载到JVM中。在这个阶段,JVM会根据类的全限定名来获取定义该类的二进制字节流,并将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。

加载过程会创建一个java.lang.Class类的实例来表示这个类。这个Class对象作为程序中每个类的数据访问入口。


链接阶段

在链接阶段,Java类加载器对类进行验证、准备和解析操作。将类与类的关系(符号引用转为直接引用)确定好,校验字节码

  1. 验证:校验类的正确性(文件格式,元数据,字节码,二进制兼容性),保证类的结构符合JVM规范。
  2. 准备:为类变量分配内存并设置类变量的默认初始值,这些变量使用的内存都在方法区中分配。(这里初始化的是类变量,即static字段,实例变量会在对象实例化时随对象一起分配在Java堆中。)
  3. 解析:把类的符号引用转为直接引用(类或接口、字段、类方法、接口方法、方法类型、方法句柄和访问控制修饰符7类符号引用 )

初始化阶段

初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 <clinit> ()方法的过程。这里利用了一种懒加载的思想,所有Java虚拟机实现(如HotSpot等)必须在每个类或接口被Java程序首次主动使用时才初始化,但类加载不一定,静态代码块在类初始化时执行

  1. 当遇到 new 、 getstatic、putstatic或invokestatic 这4条字节码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时会进行类的初始化
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时 ,如果类没初始化,需要触发其初始化
  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化
  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类
  5. 当使用 JDK1.7 的动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REFgetStatic、REFputStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化

扩展知识

什么是符号引用和直接引用

符号引用(Symbolic Reference)是一种用来表示引用目标的符号名称,比如类名、字段名、方法名等。符号引用与实际的内存地址无关,只是一个标识符,用于描述被引用的目标,类似于变量名。符号引用是在编译期间产生的,在编译后的class文件中存储。

直接引用(Direct Reference)是实际指向目标的内存地址,比如类的实例、方法的字节码等。直接引用与具体的内存地址相关,是在程序运行期间动态生成的。

假设有两个类A和B,其中A类中有一个成员变量x,B类中有一个方法foo,其中会调用A类中的成员变量x:

public class A {
    public int x;
}

public class B {
    public void foo() {
        A a = new A();
        a.x = 10;
        System.out.println("x = " + a.x);
    }
}

在B类中调用A类的成员变量x时,实际上是通过符号引用来引用A类中的x变量。在解析阶段,Java虚拟机会将A类中的符号引用转换为直接引用,定位到具体的x变量实现,并为B类生成一条指令,用于获取该变量的内存地址。

假设A类的x变量的内存地址为0x1000,在解析阶段,Java虚拟机会为B类生成一条指令,用于获取x变量的内存地址,比如:

getstatic 0x1000

这条指令会将0x1000作为直接引用,用于访问A类中的x变量。

也就是说,在类的解析阶段进行的,Java虚拟机会根据符号引用定位到具体的内存地址,并生成一条指令,用于访问该内存地址。

原文: https://www.yuque.com/hollis666/xkm7k3/tuikxhaa2urq32ds