JVM的类加载过程是Java虚拟机将类的.class
文件中的二进制数据加载到内存,并进行验证、准备、解析和初始化,最终形成可以被JVM直接使用的Java类型的过程。
这个过程至关重要,是Java语言实现跨平台、动态性 和安全性的基础。
整个类加载过程可以清晰地分为以下三个大阶段和五个详细步骤:
详细的三个阶段/五个步骤
第一阶段:Loading(加载)
目标:查找并加载类的二进制数据。
- 通过类的全限定名 (如
java.lang.String
)来获取其定义的二进制字节流(通常是.class
文件,但也可以是ZIP包、网络、运行时计算生成等)。 - 将这个字节流所代表的静态存储结构 转换为方法区(Metaspace/JDK8以前是PermGen) 中的运行时数据结构。
- 在堆内存中 生成一个代表这个类的
java.lang.Class
对象。这个Class
对象是方法区中数据的访问入口,程序通过它来访问类的类型信息、方法代码等。
注意:加载阶段与连接阶段的部分内容是交叉进行的,但逻辑上保持先后顺序。
第二阶段:Linking(连接)
连接阶段比较复杂,通常又分为三步:验证、准备和解析。
1. Verification(验证)
目标:确保被加载的类的正确性和安全性,是JVM安全的重要保障。
- 文件格式验证 :验证字节流是否符合Class文件格式的规范(如魔数
0xCAFEBABE
)、版本号是否在当前JVM支持范围内等。 - 元数据验证:对类的元数据信息进行语义校验,确保其符合Java语言规范(如:这个类是否有父类?是否继承了不允许被继承的final类?等等)。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的(如:保证方法体中的类型转换是有效的,不会出现把父类对象转成不相关的子类)。
- 符号引用验证:发生在后续的"解析"阶段,验证符号引用能否被正确找到对应的直接引用。
2. Preparation(准备)
目标 :为类变量(静态变量) 分配内存并设置初始值。
- 此时分配内存的仅包括类变量 (被
static
修饰的变量),不包括实例变量。 - 设置的初始值通常是数据类型的零值 。
- 例如:
public static int value = 123;
在准备阶段后,value
的初始值是0
,而不是123
。 - 但如果是常量(
static final
),准备阶段就会直接赋值为代码中指定的值。例如:public static final int value = 123;
在准备阶段后,value
的值就是123
。
- 例如:
3. Resolution(解析)
目标 :将常量池内的符号引用 替换为直接引用的过程。
- 符号引用 :一组符号来描述所引用的目标,与虚拟机内存布局无关。例如
java.lang.Object
就是一个符号引用。 - 直接引用:可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关。
- 解析主要针对:类或接口 、字段 、类方法 、接口方法 、方法类型 、方法句柄等符号引用。
第三阶段:Initialization(初始化)
目标 :执行类构造器 <clinit>()
方法的过程,真正开始执行类中定义的Java代码。
- 按照源码顺序,为静态变量赋值。
- 执行静态代码块。
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作 和静态代码块中的语句合并产生的。编译器收集的顺序取决于语句在源文件中出现的顺序。- 只有当类被"主动使用"时才会触发初始化,这是类加载过程的最后一步。在此阶段,JVM会确保多线程环境下类的初始化操作被正确地加锁同步。
什么情况下会触发"初始化"?(主动使用)
JVM规范严格规定了有且只有以下6种情况必须立即对类进行"初始化":
- 遇到
new
,getstatic
,putstatic
,invokestatic
这四条字节码指令时 。- 对应代码场景:使用
new
实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
- 对应代码场景:使用
- 使用
java.lang.reflect
包的方法对类进行反射调用时。 - 当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。
- 当使用JDK7新加入的动态语言支持时,如果一个
java.lang.invoke.MethodHandle
实例最后的解析结果 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。 - 当一个接口定义了JDK8新加入的默认方法(default)时,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
总结与特点
- 顺序 :加载 -> 验证 -> 准备 -> 解析 -> 初始化。注意:《Java虚拟机规范》允许解析阶段在某些情况下在初始化之后开始,这是为了支持Java的动态绑定(运行时多态)。
- 懒加载:JVM并不是在启动时就把所有类都加载完,而是按需加载(延迟加载),只有在类被"主动使用"时才会初始化。
- 双亲委派模型 :类的加载是由类加载器 完成的。JVM通过双亲委派模型(Parents Delegation Model)来组织类加载器之间的关系,从而保证了Java核心类的安全性和唯一性。
简单来说,类加载过程就是JVM将冰冷的字节码文件,转化为内存中一个活跃的、可用的类型的过程,每一步都为确保程序的稳定和安全运行提供了保障