基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

讲讲JVM 的类加载完整生命周期

知识点图片

JVM(Java虚拟机)的类加载机制是Java语言实现“一次编写,到处运行”以及动态扩展的基础。一个类的完整生命周期包含 7 个阶段,分别是:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)

其中,验证、准备、解析这三个阶段统称为链接(Linking)

为了方便记忆,你可以按照这个流程图来理解:
加载 -> [验证 -> 准备 -> 解析](链接) -> 初始化 -> 使用 -> 卸载

下面我为你详细拆解每一个阶段的核心任务和底层原理。


1. 加载(Loading)

任务:.class 文件(二进制字节流)搬到内存里,并在堆中生成一个代表这个类的 java.lang.Class 对象。

具体步骤:

  1. 通过类的全限定名获取定义此类的二进制字节流。
    (注意:JVM并没有规定必须要从本地 .class 文件中获取,它可以来自 ZIP/JAR 包、网络传输、动态代理生成(如 CGLib)、JSP 编译等。)
  2. 将这个字节流代表的静态存储结构转化为方法区(JDK 8 之后的元空间)的运行时数据结构。
  3. 在内存(堆区)中生成一个代表这个类的 java.lang.Class 对象,作为程序访问方法区中这个类各种数据的“入口”。

核心参与者: 类加载器(ClassLoader)。这里会涉及到经典的“双亲委派模型”。


2. 链接(Linking)

链接阶段的作用是将载入到 JVM 的类数据整合到 JVM 的运行环境中去。它分为三个子阶段:

2.1 验证(Verification)

任务: 保证加载进来的字节流符合 JVM 规范,不会危害虚拟机的安全。
如果你的字节码是被恶意篡改过的,JVM 会在这个阶段拒绝加载并抛出 VerifyError

  • 文件格式验证: 检查开头是不是魔数 0xCAFEBABE,主次版本号是否在当前 JVM 接受范围内等。
  • 元数据验证: 对字节码进行语义分析(例如:这个类是否有父类?是否继承了不允许被继承的 final 类?非抽象类是否实现了所有的抽象方法?)。
  • 字节码验证: 最复杂的一步,通过数据流和控制流分析,确保程序语义是合法、符合逻辑的(例如保证类型转换是安全的)。
  • 符号引用验证: 确保后续的“解析”阶段能正常执行(例如看能否通过符号引用找到对应的类、方法、字段)。

2.2 准备(Preparation)—— (面试高频考点)

任务:类变量(静态变量,被 static 修饰的变量)分配内存,并设置默认初始值(零值)

核心注意点:

  • 这里只处理类变量,不处理实例变量。实例变量会在对象实例化时分配在堆中。
  • 这里的“设置初始值”通常是指数据类型的零值(如 0, 0L, null, false)。
    • 例如: public static int value = 123; 在这个阶段,value 的值会被设为 0,而不是 123。真正的赋值 123 要等到后面的初始化阶段。
  • 例外情况(常量): 如果变量被 final 修饰(即常量),例如 public static final int value = 123;,在编译时就会生成 ConstantValue 属性,JVM 在准备阶段就会直接把 value 赋值为 123

2.3 解析(Resolution)

任务: 将常量池内的符号引用替换为直接引用

  • 符号引用: 就是一组字符串,比如 java/lang/String,或者某个方法的名字和描述符。它只是用来无歧义地定位目标。
  • 直接引用: 也就是可以直接指向目标的指针、相对偏移量或句柄。
  • 简单来说,就是把代码里的那些“名字”变成内存里真正的“内存地址”。

3. 初始化(Initialization)

任务: 真正开始执行类中定义的 Java 代码。执行类的初始化方法 <clinit>()

底层原理:

  • <clinit>() 方法是什么? 它是编译器自动收集类中的所有静态变量的赋值动作静态语句块(static {}合并产生的。
  • 执行顺序: 编译器收集的顺序是由语句在源文件中出现的次序决定的。
  • 线程安全: JVM 会保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁、同步。如果多个线程同时去初始化一个类,只会有一个线程去执行,其他线程会被阻塞。(这就是为什么用静态内部类实现单例模式是绝对线程安全的)
  • 父类优先: JVM 会保证在子类的 <clinit>() 执行前,父类的 <clinit>() 已经执行完毕。

什么时候会触发初始化?(主动引用)
JVM 规范严格规定了有且只有 6 种情况必须对类进行初始化:

  1. 遇到 new(实例化对象)、getstatic(读取静态字段)、putstatic(设置静态字段)、invokestatic(调用静态方法)这四条字节码指令时。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时(如 Class.forName("xxx"))。
  3. 初始化一个类时,如果发现其父类还没初始化,则需先触发父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
  5. 使用 JDK 7 开始提供的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例解析的结果为静态方法/字段的引用。
  6. 当一个接口中定义了 JDK 8 新加入的默认方法(被 default 修饰),如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

(注:除此之外的所有引用类的方式都不会触发初始化,称为*被动引用。例如:通过子类引用父类的静态字段,只会触发父类的初始化;通过数组定义来引用类,不会触发该类的初始化。)*


4. 使用(Using)

类初始化完成后,这个类就可以在程序中使用了。开发者可以:

  • 通过 new 关键字创建该类的实例对象。
  • 调用它的静态方法。
  • 访问它的静态变量。

5. 卸载(Unloading)

任务: 当一个类不再被使用时,JVM 会将其清理出内存(方法区/元空间),回收资源。

触发条件极其苛刻,必须同时满足以下三个条件:

  1. 该类的所有实例都已经被回收: Java 堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器已经被回收: 除非是自定义的类加载器,否则像 JDK 自带的启动类加载器、扩展类加载器、应用类加载器是不可能被回收的。
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用: 保证无法在任何地方通过反射访问该类的方法。

应用场景:
通常只有在使用自定义类加载器的场景下(如 OSGi、JSP 的热部署/热插拔、Tomcat 为每个 Web 应用创建独立的类加载器),为了防止方法区内存泄漏(OOM),才会频繁涉及到类的卸载。普通的 Java 程序运行期间,类基本不会被卸载。


总结

理解类加载的生命周期,最重要的是区分 加载(生成 Class 对象)准备(赋零值)初始化(赋实际值、执行 static 块) 这三个关键节点。这也是排查 Java 程序中诡异的类冲突、空指针异常或静态变量状态异常的理论基础。

00:00
00:00