类的生命周期
一个.class文件从被加载到JVM内存,到被卸载,一共经历了7个阶段:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载,其中验证、准备、解析统称为链接。
加载
- 通过类的全限定名来获取定义类的二进制字节流(来源包括
.class文件、Zip/Jar包、网络传输、动态代理运行时生成等)。 - 将字节流中的静态存储结构转化为方法区的运行时数据结构。
- 在堆内存中生成一个代表该类的
java.lang.Class对象,作为程序访问该类方法区中数据的外部入口。
验证
保证字节流对应的Class文件的信息符合《Java虚拟机规范》,不会危害虚拟机安全。
其中包含:
- 文件格式验证:验证二进制字节流符合Class文件格式规范,能够被当前版本JVM处理。
- 魔数检查:文件的前4个字节必须是
0xCAFEBABE。 - 版本号检查:主次版本号是否在当前JVM能够处理的范围内,不能会抛
UnsupportedClassVersionError。 - 常量池检查:常量池中的常量类型是否存在不支持的类型标签,索引是否指向了不存在的常量或不合法的常量。
- UTF-8编码检查:
CONSTANT_Utf8_info的常量中是否存在不符合UTF-8编码的数据。
- 魔数检查:文件的前4个字节必须是
- 元数据验证:验证类的元数据(语法结构和语义)是否符合Java语言规范。
- 继承合法性:该类是否有父类(除
java.Iang.Object外都有父类)。 - Final约束:该类是否继承了被
final修饰的类。 - 抽象类约束:如果该类不是抽象类,是否实现了父类或接口中要求实现的所有方法。
- 重写/重载合法性:类中的字段、方法是否与父类产生矛盾。
- 继承合法性:该类是否有父类(除
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的。
- 类型安全:保证操作数栈的数据类型与字节码指令正常配合工作。
- 跳转安全:保证方法中的控制流跳转指令(如
goto)不会跳转到方法体之外的字节码指令上。 - 类型转换安全:保证方法体中的类型转换是有效的。
- 符号引用验证:发生在解析阶段将符号引用转化为直接引用时,验证类是否缺少或无权访问其引用的外部类、字段或方法。
- 类存在性:符号引用中通过全限定名能否找到对应类。
- 成员存在性:指定类中是否存在符号引用描述和的字段或方法。
- 访问权限:符号引用中的类、字段、方法的可访问性是否允许当前类进行访问。
准备
为类的静态变量分配内存并设置初始值(零值)。
对于final修饰的基本数据类型/String常量,在编译时javac会为该变量生成ConstantValue放入常量池,准备阶段直接赋值为常量值。
其他静态变量设置初始值,如0、false、null等。
解析
将Class文件常量池中的符号引用替换为直接引用。
- 符号引用:用一组符号来描述目标,不依赖内存布局。
- 直接引用:直接指向目标的内存指针、相对偏移量或间接能够定位到目标的句柄,与JVM内存布局相关。
解析的对象包括类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等。
解析分为静态解析和动态解析(延迟解析): - 静态解析:在链接阶段就将常量池所有符号引用一次性解析完毕。
- 动态解析/延迟解析:真正使用该符号引用时才去解析该符号(如
getstatic等)。
初始化
执行类构造器<clinit>()方法的过程。
<clinit>():由编译器自动收集类中所有类变量的赋值动作和静态代码块(static{}) ,按源代码顺序合并生成<clinit>()方法。
特点:
- 执行顺序:父类的
<clinit>()先于子类<clinit>()执行。接口的<clinit>()执行时不需要先执行父接口的<clinit>()。 - 线程安全:JVM会保证一个类的
<clinit>()方法在多线程环境中被正确地加锁同步。
初始化的时机
- 字节码指令触发:遇到
new,getstatic,putstatic、invokestatic指令时。 - 反射调用:使用
java.lang.reflect包的方法对类进行反射调用时,如Class.forName()。 - 父类初始化:初始化子类时,如果父类还没初始化,先触发父类的初始化。
- 启动主类:被指定的执行主类(包含
main())会被初始化。 - 动态语言支持(JDK 7+):使用
java.lang.invoke.MethodHandle实例解析出的方法句柄对应的类未初始化时。 - 接口默认方法:初始化一个接口的实现类时,如果接口定义了默认方法,会先触发接口的初始化。
不会初始化的场景
- 通过子类引用父类的静态字段:只会触发父类的初始化,不会触发子类的初始化。
System.out.println(SubClass.superStaticField); // 只初始化 SuperClass- 通过数组引用来定义类:数组类是独立的类,对应的类不会初始化。
User[] users = new User[10]; // User 类不会被初始化,- 编译期常量:编译期会将常量存入调用类的常量池,调用时不会引用定义常量的类。
public class MyClass {
public static final String HELLO = "hello world";
// ...
}
System.out.println(MyClass.HELLO); // MyClass 不会被初始化