类生命周期(加载/链接/初始化/使用/卸载)
类的一生:从磁盘到内存
一个 Java 类从写出代码到运行在 JVM 中,要经历一个完整生命周期。这个过程就像人的一生:从出生(加载)、接受教育(链接)、步入社会(初始化)、工作生活(使用),最后离开这个世界(卸载)。
加载(Loading)
↓
链接(Linking)
├── 验证(Verification)
├── 准备(Preparation)
└── 解析(Resolution)
↓
初始化(Initialization)
↓
使用(Using)
↓
卸载(Unloading)加载(Loading)
加载阶段做了什么
加载是类生命周期的第一个阶段,它的核心任务是:
- 通过类的全限定名找到 Class 文件
- 把 Class 文件的二进制数据读入 JVM
- 在方法区/元空间创建
Class对象(java.lang.Class的实例)
Class 文件(磁盘/网络)
│
▼
类加载器读取二进制流
│
▼
创建 Class 对象
│
▼
放入方法区/元空间Class 对象的意义
加载阶段生成的 Class 对象是反射 API 的基石:
// 获取 Class 对象的几种方式
Class<?> clazz1 = String.class; // 字面量方式
Class<?> clazz2 = "hello".getClass(); // 实例获取
Class<?> clazz3 = Class.forName("java.lang.String"); // forName 方式Class 对象就像类的「镜子」,它包含了类的所有结构信息,Java 的反射 API 都是基于它实现的。
类加载器
类加载器(ClassLoader)是负责加载类的组件:
| 类加载器 | 负责加载 | 位置 |
|---|---|---|
| Bootstrap ClassLoader | 核心 Java 类库(java.*、javax.*) | JVM 内置,C++ 实现 |
| Extension ClassLoader | jre/lib/ext 目录下的类 | Java 实现 |
| Application ClassLoader | classpath 上的类 | Java 实现 |
| 自定义类加载器 | 用户自定义的加载逻辑 | 用户实现 |
Application ClassLoader
↑
│ 父加载器
Extension ClassLoader
↑
│ 父加载器
Bootstrap ClassLoader每个类加载器都有自己的命名空间,同一个类被不同类加载器加载后,是两个完全不同的类。
链接(Linking)
链接是验证、准备和解析三个子阶段的统称。
1. 验证(Verification)
验证 Class 文件的字节流是否符合 JVM 规范。这是 JVM 的「安检」环节,防止恶意代码或损坏的 Class 文件。
验证阶段检查的内容:
文件结构验证
├── 魔数是否正确(0xCAFEBABE)
├── 主版本号是否在 JVM 支持范围内
├── 常量池常量类型是否合法
└── 验证是否有足够的字节
语义验证
├── 类是否有父类(除了 Object)
├── 类是否继承了 final 类
├── 方法是否实现了抽象方法
└── 类的方法是否有正确签名
字节码验证
├── 指令是否合法
├── 操作数栈是否不会溢出
├── 局部变量使用前是否已初始化
└── 类型转换是否安全// 验证失败的例子:自定义 Class 文件(魔数错误)
# 某个被篡改的 class 文件
# 0xCAFEBABE 被改成了 0xCAFEBEEF
# → 抛出 java.lang.ClassFormatError2. 准备(Preparation)
为类的静态变量分配内存,并设置默认初始值。
准备阶段:
public static int count = 100; // 分配内存,默认值 = 0(不是 100)
public static final int MAX = 200; // 分配内存,默认值 = 0(常量在编译时常量池中)
public static Object obj; // 分配内存,默认值 = null
注意:这时候还没有执行赋值语句,只是分配内存并置零值。关键点:准备阶段只会给 static 变量分配内存和设置零值,= 100 这种赋值是在初始化阶段做的。
3. 解析(Resolution)
把符号引用替换为直接引用。
编译时(Class 文件中):
使用符号引用(不知道实际内存地址)
┌──────────────────────────────────┐
│ getfield #field_index │
│ invokevirtual #method_index │
│ new #class_index │
└──────────────────────────────────┘
运行时(解析后):
替换为直接引用(实际的内存地址)
┌──────────────────────────────────┐
│ getfield 0x0000F080 │
│ invokevirtual 0x0000F088 │
│ new [内存地址] │
└──────────────────────────────────┘解析的时机是灵活的:
- 立即解析:类加载时就把所有符号引用解析完
- 延迟解析:第一次使用时才解析(JVM 规范允许,但 HotSpot 实际是立即解析)
解析的主要类型:
符号引用 → 直接引用
类/接口:CONSTANT_Class → 指向方法区的指针
字段: CONSTANT_Fieldref → 指向字段的内存偏移量
方法: CONSTANT_Methodref → 指向方法的入口地址初始化(Initialization)
初始化阶段做什么
初始化阶段是类加载过程的最后一步。这阶段会执行类构造器 <clinit>(),它是编译器自动收集所有静态变量的赋值语句和静态代码块合并生成的。
// 源码
public class User {
public static int count = 100;
public static String name = "Alice";
static {
System.out.println("User 类初始化");
count = 200;
}
}编译后生成的 <clinit>():
// 编译器生成的 <clinit>() 方法
static {
count = 100; // 静态变量赋值
name = "Alice"; // 静态变量赋值
System.out.println("User 类初始化"); // 静态代码块
count = 200; // 静态代码块中的赋值
}<clinit>() vs <init>()
| 方法 | 作用 | 谁生成 |
|---|---|---|
<clinit>() | 类构造器,初始化 static 变量和静态块 | 编译器自动生成 |
<init>() | 实例构造器,初始化实例 | 源码中的构造函数 |
public class Demo {
static { x = 1; } // <clinit> 中
int y = 2; // <init> 中
Demo() { // <init> 中
super();
x = 3;
y = 4;
}
}触发初始化的 6 种时机
JVM 规范定义了触发类初始化的 6 种「主动引用」:
1. new 创建实例 → new Object()
2. 访问静态字段 → System.out(访问 static 字段)
3. 调用静态方法 → ClassName.staticMethod()
4. 反射调用 → Class.forName("...")
5. 初始化子类 → 父类会先被初始化
6. 主类(main 所在类) → 程序入口不会触发初始化的场景
// 不会触发初始化(只是加载和链接):
Class clazz = String.class; // 字面量获取 Class
Class.forName("java.lang.String", false, loader); // 第二个参数 false = 不初始化
子类访问父类的静态字段 // 子类不初始化,父类会
通过数组类型引用 // new String[10] 不触发 String 初始化
常量(编译时常量) // static final int MAX = 100(无副作用)被动引用示例
// 示例 1:通过子类访问父类的静态字段
class Parent {
static { System.out.println("Parent 初始化"); }
static int value = 100;
}
class Child extends Parent {
static { System.out.println("Child 初始化"); }
}
// main:
Child.value; // 只输出 "Parent 初始化",Child 不初始化!// 示例 2:访问编译时常量
class Const {
static final int MAX = 100; // 编译时常量
static final String STR = "hello"; // 编译时常量
}
System.out.println(Const.MAX); // 不触发初始化,常量在编译时就内联了// 示例 3:通过数组引用
String[] arr = new String[10]; // 不触发 String 初始化使用(Using)
类的使用就是程序正常运行——创建对象、调用方法、访问字段等。这是最长的阶段,也是我们最熟悉的阶段。
卸载(Unloading)
卸载的条件
一个类可以被卸载,当且仅当:
1. 该类的所有实例都已被回收(没有活引用)
2. 该类的 Class 对象没有被引用(没有反射持有)
3. 该类的类加载器已被回收类加载器被回收 ← 该加载器加载的所有类都没有活引用
↑
该类所有实例被回收
↑
没有反射持有该类的 Class 对象三类加载器的卸载情况
| 类加载器 | 卸载情况 |
|---|---|
| Bootstrap ClassLoader | 永不卸载(JVM 核心类) |
| Extension ClassLoader | 一般不卸载 |
| Application ClassLoader | 可以卸载(GC 可以回收) |
自定义类加载器的卸载
自定义类加载器加载的类满足条件时可以卸载:
// 自定义类加载器加载的类可以被卸载
public class MyClassLoader extends ClassLoader {
// 加载类...
}
// 当 MyClassLoader 实例和它加载的所有类都没有引用时
// 这些类就可以被 GC 回收类卸载的意义
类卸载可以释放内存和实现热更新:
类的元数据(方法区/元空间)
│
├── 如果类不卸载:元数据会一直占用内存
│
└── 如果类可以卸载:元数据可以被回收
热更新(OSGi、Tomcat 热部署)就是利用了类卸载机制:
1. 创建新的类加载器加载新版本类
2. 旧版本类没有引用后,旧类加载器可以被回收
3. 旧版本类随之被卸载,释放元空间类生命周期的时序图
时间线
加载 ─── 验证 ─── 准备 ─── 解析 ─── 初始化 ─── 使用 ─── 卸载
│ │ │ │ │ │
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
读取 检查 分配 符号 执行 正常运行 条件满足
Class 字节码 静态内存 引用 <clinit> 时卸载
文件 合法性 零值初始化 转 静态
安检 内存分配 直接引用 赋值
│
│ 可在初始化后
│ 也可更早本节小结
类生命周期的核心要点:
| 阶段 | 核心任务 | 主要参与者 |
|---|---|---|
| 加载 | 读取 Class 文件,创建 Class 对象 | ClassLoader |
| 验证 | 检查 Class 文件合法性 | JVM |
| 准备 | 分配静态内存,设置零值 | JVM |
| 解析 | 符号引用 → 直接引用 | JVM |
| 初始化 | 执行 <clinit>(),静态赋值 | JVM(触发主动引用时) |
| 使用 | 创建实例、调用方法 | 应用程序 |
| 卸载 | 满足条件时回收类元数据 | GC |
主动引用触发初始化,被动引用不触发。类卸载需要类加载器、实例和 Class 对象都没有引用才能发生。
下一节,我们来看 类初始化方法 clinit()(线程安全)。
