目录
(1)加载(Loading):类加载的"入口",获取并存储类的二进制数据
(2)验证(Verification):类加载的"安检",保证字节码的合法性
[(3) 准备(Preparation):为类变量分配内存,赋默认值](#(3) 准备(Preparation):为类变量分配内存,赋默认值)
(4)解析(Resolution):将符号引用转换为直接引用
(5)初始化(Initialization):执行用户的初始化逻辑
(3)该类的java.lang.Class对象没有被任何地方引用
[(2)GC 的不确定性](#(2)GC 的不确定性)
[(3)元空间的影响(JDK 8+)](#(3)元空间的影响(JDK 8+))
一、类的生命周期
Java 中类的生命周期是指类从被加载到 JVM 中开始,到最终被卸载出内存为止的整个过程 。根据《Java 虚拟机规范》,类的生命周期可分为7 个阶段 ,其中加载、验证、准备、解析、初始化 属于类加载阶段,而使用和卸载是类加载后的运行时阶段。
顺序如下:
加载(Loading)→ 验证(Verification)→ 准备(Preparation)→ 解析(Resolution)→ 初始化(Initialization)→ 使用(Using)→ 卸载(Unloading)
其中:
- 加载、验证、准备、初始化、卸载 是固定顺序执行的(解析阶段可能在初始化之后执行,为了支持动态绑定);
- 只有初始化阶段 是主动触发的(由 JVM 或用户代码触发),其余阶段大多是被动触发的。
二、类加载过程
Java 的类加载过程 是类生命周期中加载、验证、准备、解析、初始化 这五个阶段的统称(其中解析阶段可灵活调整时机),是 JVM 将.class文件的二进制数据转化为内存中可执行类的核心流程。这个过程由类加载器和 JVM 协同完成,每个阶段都有明确的职责和严格的执行规范。
1、类加载过程的流程
类加载过程遵循固定的执行顺序(解析阶段可延迟),整体流程如下:
加载(Loading)→ 验证(Verification)→ 准备(Preparation)→ 解析(Resolution)→ 初始化(Initialization)
- 核心特征 :前四个阶段(加载 - 解析)主要由 JVM 自动完成,初始化阶段是唯一执行用户代码(静态代码块、类变量赋值)的阶段;
- 解析的灵活性 :解析阶段可在初始化阶段之前 (静态绑定)或之后(动态绑定)执行,以支持 Java 的多态特性。
2、各阶段详解
(1)加载(Loading):类加载的"入口",获取并存储类的二进制数据
这是类加载的第一个阶段,由类加载器(ClassLoader) 负责,核心是将.class文件的二进制数据加载到 JVM 内存,并创建Class对象。
主要任务:
1)获取类的二进制字节流:
JVM 不限制字节流的来源,常见方式包括:
- 从本地文件系统加载(如
Hello.class,最常见); - 从 JAR/ZIP 包加载(如
rt.jar中的java.lang.String); - 从网络加载(如早期的 Applet 应用);
- 动态生成(如 ASM、CGLIB 生成的字节码,动态代理的代理类);
- 从数据库、缓存等其他数据源加载。
2)将字节流转换为方法区的运行时数据结构:
JVM 会按照.class文件的结构规范,将字节流中的常量池、类信息、方法表、字段表 等数据,存储到方法区 (JDK 8 及以后为元空间(Metaspace),JDK 7 及以前为永久代)。
3)在堆中创建java.lang.Class对象:
这个对象是类的元数据访问入口 ,包含了类的全限定名、方法、字段等信息,后续通过反射操作类时,本质就是操作这个Class对象。
注意:Class对象是单例 的(同一个类在 JVM 中只有一个Class对象),由类加载器保证其唯一性。
核心靠类加载器实现
类加载器有很多种,具体使用哪种类加载器是由双亲委托模型决定的。JVM 提供了三层内置类加载器,还有用户自定义的类加载器:
| 类加载器类型 | 负责加载的类 | 实现语言 | 父类加载器 |
|---|---|---|---|
| 启动类加载器(Bootstrap) | JRE/lib/rt.jar 等核心类库(如 java.lang.*) | C++ | 无(根加载器) |
| 扩展类加载器(Extension) | JRE/lib/ext 目录下的扩展类 | Java | 启动类加载器 |
| 应用程序类加载器(Application) | 类路径(ClassPath)下的用户自定义类 | Java | 扩展类加载器 |
| 自定义类加载器 | 自定义路径的类(如热部署、加密类) | Java | 应用程序类加载器 |
(2)验证(Verification):类加载的"安检",保证字节码的合法性
这是类加载的安全屏障,JVM 会对加载的字节码进行多维度校验,防止恶意或非法的字节码破坏 JVM 的运行安全。若校验失败,会直接抛出ClassFormatError、VerifyError等异常。
验证阶段由四个校验环节组成:
1)文件格式验证(最基础)
校验.class文件的二进制格式是否符合 JVM 规范,包括:
- 魔数是否为
0xCAFEBABE; - 主版本号是否在 JVM 支持的范围内;
- 字节码的结构是否完整(如常量池的索引是否有效)。
2)元数据验证(语义检验)
校验类的元数据是否符合 Java 语言的语法规范,包括:
- 类是否继承了
final类(若继承则非法); - 类是否实现了接口的所有抽象方法;
- 字段、方法的签名是否合法(如方法参数类型是否有效)。
3)字节码验证(最复杂)
校验字节码指令的执行逻辑是否合法,防止运行时出现内存越界、栈溢出等问题,包括:
- 操作数栈的栈深度是否平衡(如压入两个数后必须执行弹出操作);
- 是否访问了越界的局部变量表;
- 指令的跳转是否指向合法的代码行。
4)符号引用验证(提前预判)
校验常量池中的符号引用是否能被解析为有效的目标,包括:
- 引用的类、方法、字段是否存在;
- 访问权限是否合法(如是否试图访问私有方法)。
文件格式验证是基于类的二进制字节流进行的,主要是保证输入的字节流能正确的解析并存储于方法去之内,在格式上符合描述一个Java类型信息的要求。后面三个校验阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流。
符号引用验证是发生在类加载过程中的解析阶段,即JVM将符号引用直接转化为直接引用的时候,目的是为了保证解析阶段正常执行。
开发环境中可通过-Xverify:none参数关闭验证阶段,提升类加载速度(生产环境禁止使用 ,会带来安全风险)。但是 -Xverify:none 和 -noverify在JDK13被标记为了deprecated,在未来的JDK版本中可能会被移除。
(3) 准备(Preparation):为类变量分配内存,赋默认值
该阶段在方法区 中为类的静态变量(类变量) 分配内存,并设置默认初始值(零值),是 JVM 自动完成的内存初始化步骤。
注意:
-
仅处理静态变量:实例变量的内存分配在对象实例化时(JDK7之前在永久代中,JDK7以后在堆中)进行,与该阶段无关。
-
默认值为数据类型的零值:
数据类型 默认零值 数据类型 默认零值 int 0 boolean false long 0L char '\u0000' float 0.0f 引用类型 null double 0.0d byte/short 0 -
static final常量的特殊处理 :若静态变量被final修饰(如public static final int NUM = 10;),则在准备阶段直接赋值为代码中定义的常量值 ,而非零值。原因是:static final常量的值在编译期已确定,存储在.class文件的ConstantValue属性中,准备阶段直接从该属性取值赋值。
实例代码对比:
public class Test {
public static int a = 10; // 准备阶段赋值为0,初始化阶段赋值为10
public static final int b = 20; // 准备阶段直接赋值为20
}
(4)解析(Resolution):将符号引用转换为直接引用
该阶段将方法区中常量池的符号引用 (字符串形式的引用)转换为直接引用(内存地址、偏移量等),是 JVM 实现动态链接的关键。
理解:
- 符号引用 :以字符串形式表示的引用,如
java/lang/System.out、java/io/PrintStream.println,与具体内存地址无关。 - 直接引用:指向内存中实际对象的指针、偏移量或句柄,与 JVM 的内存布局相关。
解析的内容:
- 类 / 接口解析 :将类名的符号引用转换为对应的
Class对象; - 字段解析:将字段名的符号引用转换为指向字段的直接引用;
- 方法解析:将方法名的符号引用转换为指向方法的直接引用;
- 接口方法解析:将接口方法的符号引用转换为指向接口方法的直接引用。
何时解析:
- 静态解析 :在解析阶段完成(适用于静态绑定 的方法,如
static方法、final方法、私有方法,这些方法的调用目标在编译期即可确定); - 动态解析 :延迟到运行时 完成(适用于动态绑定的方法,如实例方法,为了支持多态,调用目标需在运行时根据对象的实际类型确定)。
(5)初始化(Initialization):执行用户的初始化逻辑
这是类加载过程中唯一执行用户代码的阶段 ,是执行初始化方法<clinit>()方法的过程,<clinit>()方法是由静态代码块(static {}) 和静态变量的赋值语句合并得到的,完成类的最终初始化。
<clinit>()方法方法是编译之后自动生成的。
对于<clinit>()方法的调用,虚拟机会确保自己在多线程环境的安全性。因为<clinit>()方法有线程安全锁,在多线程环境进行初始化可能引起多个线程阻塞且这种阻塞很难被发现。
初始化的触发条件:主动引用(必须触发)
JVM 严格规定,只有发生主动引用 时,才会触发类的初始化;被动引用不会触发初始化。
1)主动引用的场景:
- 创建类的实例(如
new Test()); - 调用类的静态方法(如
Test.staticMethod()); - 访问类的非
final静态变量(如System.out.println(Test.a)); - 反射调用类的方法(如
Class.forName("com.example.Test")); - 初始化子类时,父类会先被初始化(递归到
java.lang.Object); - JVM 启动时的主类(包含
main方法的类,必然被初始化)。
2)被动引用的场景(不触发初始化):
- 访问类的
static final常量(常量在编译期已存入常量池,无需初始化类); - 通过子类访问父类的静态变量(仅初始化父类,子类不初始化);
- 创建类的数组(如
Test[] arr = new Test[10],仅创建数组对象,不初始化Test类)。
初始化的执行顺序:
- 父类优先 :先初始化父类(递归向上,直到
java.lang.Object); - 顺序执行:按代码定义的顺序,执行静态变量赋值语句和静态代码块(多个静态代码块按定义顺序执行)。
代码示例:
java
public class Parent {
public static int parent = 10;
static {
System.out.println("Parent 静态代码块");
}
}
public class Child extends Parent {
public static int child = 20;
static {
System.out.println("Child 静态代码块");
}
public static void main(String[] args) {
System.out.println(Child.child);
}
}
运行结果如下:

启动main方法触发Child类的初始化,先初始化父类Parent,再执行Child的静态代码和变量赋值。
三、类卸载
在 Java 中,类卸载 是类生命周期的最后一个阶段,指 JVM 将类的元数据从方法区(元空间,JDK 8+) 中移除,同时回收堆中对应的java.lang.Class对象,释放类占用的内存资源。类卸载的条件非常严格,这也是为什么我们通常感觉 "类似乎不会被回收" 的原因。
1、类卸载的条件
JVM 规范明确规定,只有满足以下三个条件时,类才可能被卸载,三者缺一不可:
(1)该类的所有实例对象都已被回收
堆中不存在该类的任何实例(包括正常实例、数组实例等),实例对象的内存已被 GC 回收。
(2)加载该类的类加载器已被回收
类加载器的实例本身也是一个 Java 对象,若它没有被任何地方引用(如自定义类加载器的实例被 GC 回收),则其加载的类才有卸载的可能。
- 注意:JVM内置的类加载器(启动类加载器、扩展类加载器、应用程序类加载器) 始终存在于 JVM 的生命周期中,永远不会被回收。
(3)该类的java.lang.Class对象没有被任何地方引用
堆中的Class对象是类元数据的访问入口,若它被反射、静态变量、集合等引用持有,则类无法被卸载。
2、类卸载的范围
类卸载的条件决定了其仅适用于自定义类加载器加载的类 ,而JVM 内置类加载器加载的核心类库(如java.lang.String、java.util.ArrayList)永远不会被卸载。
| 类加载器类型 | 加载的类是否可被卸载 | 原因 |
|---|---|---|
| 启动类加载器 | 否 | 加载核心类库,类加载器本身由 C++ 实现,常驻 JVM 内存,Class对象被永久引用 |
| 扩展类加载器 | 否 | JVM 启动后始终存在,其加载的类的Class对象被系统持有 |
| 应用程序类加载器 | 基本否 | 加载应用程序的核心类,通常随应用进程存活,类加载器不会被回收 |
| 自定义类加载器 | 是 | 满足三个条件时,类加载器和其加载的类均可被回收 |
3、类卸载的注意事项
(1)避免持有Class对象的引用
若代码中通过static变量、集合、反射缓存等方式持有Class对象的引用,即使满足其他条件,类也无法被卸载。
如:
java
// 该静态变量持有Class对象,导致类无法卸载
public static Class<?> cacheClass;
(2)GC 的不确定性
JVM 的 GC 是自动触发的,调用System.gc()仅为建议,不保证立即执行。因此类卸载可能存在延迟,需多次 GC 验证。
(3)元空间的影响(JDK 8+)
JDK 8 将方法区替换为元空间(Metaspace),元空间使用本地内存而非堆内存,类的元数据存储在元空间中。类卸载时,元空间中的类元数据也会被释放,避免了永久代的内存溢出问题。
(4)接口和父类的卸载
类卸载时,其实现的接口和父类不会被自动卸载,除非接口 / 父类也满足类卸载的三个条件。
4、与实例回收的区别
| 特征 | 实例回收 | 类卸载 |
|---|---|---|
| 回收对象 | 堆中的实例对象 | 方法区的类元数据 + 堆中的Class对象 |
| 触发条件 | 实例无引用,GC 时回收 | 满足三个严格条件,GC 时回收 |
| 回收频率 | 频繁发生 | 极少发生(仅自定义类加载器的类) |
| 影响范围 | 单个实例 | 整个类(所有实例已回收) |