类的生命周期


一个.class文件从被加载到JVM内存,到被卸载,一共经历了7个阶段:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载,其中验证、准备、解析统称为链接

加载

  1. 通过类的全限定名来获取定义类的二进制字节流(来源包括.class文件、Zip/Jar包、网络传输、动态代理运行时生成等)。
  2. 将字节流中的静态存储结构转化为方法区的运行时数据结构。
  3. 在堆内存中生成一个代表该类的java.lang.Class对象,作为程序访问该类方法区中数据的外部入口。

验证

保证字节流对应的Class文件的信息符合《Java虚拟机规范》,不会危害虚拟机安全。
其中包含:

  • 文件格式验证:验证二进制字节流符合Class文件格式规范,能够被当前版本JVM处理。
    • 魔数检查:文件的前4个字节必须是0xCAFEBABE
    • 版本号检查:主次版本号是否在当前JVM能够处理的范围内,不能会抛UnsupportedClassVersionError
    • 常量池检查:常量池中的常量类型是否存在不支持的类型标签,索引是否指向了不存在的常量或不合法的常量。
    • UTF-8编码检查CONSTANT_Utf8_info的常量中是否存在不符合UTF-8编码的数据。
  • 元数据验证:验证类的元数据(语法结构和语义)是否符合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>()方法在多线程环境中被正确地加锁同步。

初始化的时机

  1. 字节码指令触发:遇到newgetstaticputstaticinvokestatic指令时。
  2. 反射调用:使用java.lang.reflect包的方法对类进行反射调用时,如Class.forName()
  3. 父类初始化:初始化子类时,如果父类还没初始化,先触发父类的初始化。
  4. 启动主类:被指定的执行主类(包含main())会被初始化。
  5. 动态语言支持(JDK 7+):使用java.lang.invoke.MethodHandle实例解析出的方法句柄对应的类未初始化时。
  6. 接口默认方法:初始化一个接口的实现类时,如果接口定义了默认方法,会先触发接口的初始化。

不会初始化的场景

  1. 通过子类引用父类的静态字段:只会触发父类的初始化,不会触发子类的初始化。
System.out.println(SubClass.superStaticField); // 只初始化 SuperClass
  1. 通过数组引用来定义类:数组类是独立的类,对应的类不会初始化。
User[] users = new User[10]; // User 类不会被初始化,
  1. 编译期常量:编译期会将常量存入调用类的常量池,调用时不会引用定义常量的类。
public class MyClass {
	public static final String HELLO = "hello world";
	// ...
}
System.out.println(MyClass.HELLO); // MyClass 不会被初始化