类载入过程介绍
类加载过程是Java虚拟机(JVM)将类的.class文件中的二进制数据读入内存,并将其转换为java.lang.Class对象的过程。这个过程是Java程序能够运行的基础,因为它使得程序可以访问和使用类的静态成员、创建类的实例以及调用类的方法。
JVM的类加载过程主要包括以下五个阶段,通常是按顺序进行的,但解析阶段在某些情况下也可能在初始化之后再发生(为了支持Java语言的动态绑定):
-
加载(Loading):
- 目的:查找并加载类的二进制数据(
.class文件)。 - 过程:
a. 通过一个类的全限定名(例如
com.example.MyClass)获取定义此类的二进制字节流。这个字节流可以从多种来源获取,如本地文件系统(最常见)、网络(如Applet)、JAR/ZIP等归档文件、运行时动态生成(如动态代理技术)、由其他文件生成(如JSP文件编译成的Servlet类)等。这个任务由类加载器(Class Loader)完成。 b. 将这个字节流所代表的静态存储结构转化为方法区(或元空间)的运行时数据结构。 c. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。 - 注意:加载阶段与连接阶段的部分内容(如验证字节码文件格式)是交叉进行的。
- 目的:查找并加载类的二进制数据(
-
连接(Linking): 连接阶段是将已经加载的类的二进制数据合并到JVM的运行时状态中。它又分为三个子阶段:
- a. 验证(Verification):
- 目的:确保被加载的类(
.class文件)的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 - 内容:包括文件格式验证(如魔数、主次版本号)、元数据验证(如是否有父类、是否继承了不允许被继承的类)、字节码验证(通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的)、符号引用验证(确保符号引用能转换为直接引用)等。
- 这是JVM类加载机制中一个非常重要的环节,对防止恶意代码破坏至关重要。
- 目的:确保被加载的类(
- b. 准备(Preparation):
- 目的:为类的静态变量(static fields)分配内存,并设置其初始值(零值)。
- 过程:在这个阶段,JVM会在方法区(或元空间)中为类的静态变量分配内存,并赋予这些变量一个初始的默认值。例如,
int类型的静态变量会被初始化为0,boolean为false,引用类型为null。 - 注意:这里设置的是数据类型的零值,而不是程序员在代码中为静态变量显式赋的值。显式赋值操作是在后续的初始化阶段的
<clinit>()方法中执行的。但是,如果静态变量是final类型的常量(static final),并且其值在编译时就能确定(即常量值),那么在准备阶段就会被直接赋值为代码中指定的值(例如public static final int VALUE = 123;,VALUE在准备阶段就为123)。
- c. 解析(Resolution):
- 目的:将常量池中的符号引用(Symbolic References)替换为直接引用(Direct References)。
- 过程:符号引用是一种以一组符号来描述所引用的目标的,引用的目标并不一定已经加载到内存中。直接引用则是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行。
- 解析阶段的发生时机并不固定,它可能在初始化之前完成,也可能在初始化之后(例如,通过反射动态加载类或使用
invokedynamic指令时,相关的符号引用解析会推迟到运行时)。
- a. 验证(Verification):
-
初始化(Initialization):
- 目的:执行类的初始化代码,即执行类构造器
<clinit>()方法。 - 过程:
<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态初始化块(static {}块)中的语句合并产生的。编译器收集的顺序是由语句在源文件中出现的顺序决定的。 <clinit>()方法与类的构造函数(即实例构造器<init>()方法)不同:<clinit>()不需要显式调用父类的<clinit>()方法,JVM会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此,在JVM中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态变量赋值操作或者静态初始化块,那么编译器可以不为这个类生成<clinit>()方法。- 接口中也会有
<clinit>()方法(当接口中有默认方法或静态成员时),但与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当真正使用到父接口的时候(如引用接口中定义的常量)才会触发父接口的初始化。 - JVM会保证一个类的
<clinit>()方法在多线程环境中被正确地加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞。
- 触发初始化的时机:JVM规范严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
- 遇到
new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候、以及调用一个类的静态方法的时候。 - 使用
java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。 - 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含
main()方法的那个类),虚拟机会先初始化这个主类。 - 当使用JDK 7的动态语言支持时,如果一个
java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。 - 当一个接口中定义了JDK 8新加入的默认方法(被
default关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
- 遇到
- 目的:执行类的初始化代码,即执行类构造器
这五个阶段共同完成了将一个.class文件中的信息加载到JVM内存中,并使其可以被程序使用的全过程。