前言
Java 类加载是 JVM 核心底层机制之一,本质是将外部.class字节码文件加载至 JVM 内存,完成字节码→类元数据→可执行 Class 对象的转化过程,是 Java 程序运行的前置基础。
类加载的完整生命周期分为 加载(Loading)→ 链接(Linking)→ 初始化(Initialization)→ 使用(Using)→ 卸载(Unloading) 五个阶段,其中加载、链接、初始化是核心执行阶段(链接又细分为验证、准备、解析三个子阶段),也是面试与底层学习的核心考点。
一、加载(Loading):字节码→Class 对象
1. 核心作用
将类的字节码数据从外部存储介质读取到 JVM 内存,在方法区(JDK8 + 为元空间) 生成该类的元数据信息,同时在堆区 创建唯一的Class对象 ------ 该对象是 JVM 中访问类元数据的唯一入口。
2. 字节码的常见加载来源
| 加载来源 | 典型应用场景 |
|---|---|
| 本地文件系统 | 项目编译后classpath下的.class文件 |
| 压缩包(JAR/WAR/EAR) | Maven/Gradle 引入的第三方依赖(如 mysql-connector.jar) |
| 网络传输 | 早期 Applet 程序、远程 RPC 框架(如 Dubbo)的类传输 |
| 动态生成 | 反射、JDK 动态代理、ASM/CGLIB 字节码框架动态构建 |
| 数据库 / 磁盘加密文件 | 自定义类加载器加载加密字节码,实现类的安全防护 |
3. 类加载器的层次结构(双亲委派模型)
JVM 内置三类核心类加载器,严格遵循双亲委派模型(父加载器优先加载),该模型是保证 JVM 内类的唯一性、保护核心类库安全的关键。
注:JDK9 + 引入模块系统后,类加载器命名略有调整,但核心逻辑与双亲委派模型不变。
类加载器层次结构与核心属性表
| 类加载器类型 | 实现语言 | 核心加载路径 | 父加载器 | 关键特点 |
|---|---|---|---|---|
| 启动类加载器(Bootstrap ClassLoader) | C++ | $JAVA_HOME/jre/lib(如 rt.jar、core.jar 等核心类库) |
无(根加载器) | 无对应的 Java 类,getClassLoader() 返回 null |
| 扩展类加载器(Extension ClassLoader) | Java | $JAVA_HOME/jre/lib/ext 或 java.ext.dirs 系统属性指定路径 |
启动类加载器 | 加载 JVM 扩展功能的类库,提供核心类库外的扩展能力 |
| 应用类加载器(Application ClassLoader) | Java | 应用 classpath 路径(-cp/classpath 命令行参数指定) |
扩展类加载器 | 自定义类加载器的默认父加载器,负责加载项目业务类与第三方依赖类 |
4. 双亲委派机制(核心规则)
执行逻辑
类加载器接收到类加载请求时,不会立即自行加载 ,而是先将请求向上委派给父加载器 ;父加载器在自身搜索范围内尝试加载,若加载失败(未找到类),则将请求向下回传,由当前加载器自行加载。
核心优势
- ✅ 保证类的唯一性:全限定名相同的类,由同一父加载器加载,避免 JVM 内存中出现多个相同的类元数据;
- ✅ 保护核心类库安全 :避免自定义
java.lang.String、java.lang.Object等核心类被加载,防止恶意篡改核心类逻辑。
5. 自定义类加载器
适用场景
- 加载非标准来源的字节码(如加密的
.class、数据库 / 网络读取的字节码); - 实现类的热部署 / 类隔离(如 Tomcat 的 WebAppClassLoader,实现不同 Web 应用的类隔离);
- 突破
classpath限制,加载指定磁盘路径的类。
实现原则
推荐重写findClass()方法,而非loadClass() ------ 直接重写loadClass()会破坏双亲委派模型,findClass()是 ClassLoader 为自定义加载预留的方法,仅在父加载器加载失败后执行,完美遵循双亲委派。
完整可运行代码
自定义类加载器核心类
java
package com.dwl.ex01_类加载子系统;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* @Description 实现从指定磁盘路径加载 class 字节码文件的自定义类加载器
* 自定义类加载器
* 继承 JDK 的 ClassLoader,重写 findClass 方法实现自定义路径的类加载
* 核心是 classLoader 的 defineClass 方法将字节数组转为 JVM 可识别的 Class 对象
* 注意:推荐重写findClass而非loadClass,避免破坏双亲委派模型
* @Version 1.0.0
* @Date 2026/2/6 1:38
* @Author Dwl
*/
public class CustomClassLoader extends ClassLoader {
/**
* 重写ClassLoader的findClass方法
* 当双亲委派模型中父类加载器均无法加载类时,会调用此方法进行自定义加载
*
* @param name 要加载的类的全限定类名
* @return 加载成功的Class对象,由defineClass方法将字节码转换而来
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 定义自定义class文件的磁盘路径
String classFilePath = "F:\\idea-workspase\\JVM-Explorer\\JDK17\\CustomClass.class";
// 读取class文件的字节数组(核心:将磁盘上的字节码文件读入内存)
byte[] classBytes = Files.readAllBytes(Paths.get(classFilePath));
/*
* 调用ClassLoader的核心方法defineClass,将字节数组转为Class对象
* 此方法为native方法,由JVM实现,负责将字节码解析为JVM可识别的Class元数据
* 参数说明:
* @param name 类的全限定名,可为null(本示例直接传入参数name)
* @param b 类的字节码数组
* @param off 字节数组的起始偏移量(从0开始)
* @param len 字节数组的有效长度(字节码的实际长度)
*/
return defineClass(name, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("自定义类加载器加载类失败,类名:" + name, e);
}
}
}
待加载的测试类(无包名)
java
/**
* @Description javac -encoding UTF-8 F:\idea-workspase\JVM-Explorer\JDK17\CustomClass.java
* @Version 1.0.0
* @Date 2026/2/6 1:48
* @Author Dwl
*/
public class CustomClass {
private String msg = "自定义类加载器加载成功!";
public CustomClass() {
System.out.println("CustomClass 无参构造器执行");
}
public void showMsg() {
System.out.println("测试方法执行:" + msg);
}
public String sayHello(String name) {
return "Hello " + name + ",我是被自定义类加载器加载的类!";
}
}
测试类(验证自定义加载逻辑)
java
package com.dwl.ex01_类加载子系统;
import java.lang.reflect.Method;
/**
* @Description 自定义类加载器的测试类
* @Version 1.0.0
* @Date 2026/2/6 1:49
* @Author Dwl
*/
public class TestCustomClassLoader {
public static void main(String[] args) {
try {
// 创建自定义类加载器的实例
CustomClassLoader customClassLoader = new CustomClassLoader();
// 调用loadClass方法加载类(而非直接调用findClass)
// 原因:loadClass是ClassLoader的入口方法,会遵循双亲委派模型,先让父类加载器尝试加载
// 父类加载器(扩展类加载器、启动类加载器)无法加载D盘的类,最终会调用我们重写的findClass
Class<?> clazz = customClassLoader.loadClass("CustomClass");
// 打印类的信息和加载该类的类加载器(验证是否为自定义类加载器)
System.out.println("类的全限定名:" + clazz.getName());
System.out.println("加载该类的类加载器:" + clazz.getClassLoader());
// 系统类加载器(AppClassLoader)
System.out.println("自定义类加载器的父类加载器:" + customClassLoader.getParent());
System.out.println("------------------------");
// 通过反射创建类的实例(调用无参构造器)
Object instance = clazz.getDeclaredConstructor().newInstance();
// 通过反射调用无参方法showMsg()
Method showMsgMethod = clazz.getMethod("showMsg");
showMsgMethod.invoke(instance);
// 通过反射调用有参方法sayHello(String)
Method sayHelloMethod = clazz.getMethod("sayHello", String.class);
Object result = sayHelloMethod.invoke(instance, "Java学习者");
System.out.println("有参方法返回结果:" + result);
} catch (Exception e) {
System.out.println("异常信息:" + e);
}
}
}
运行结果
tex
类的全限定名:CustomClass
加载该类的类加载器:com.dwl.ex01_类加载子系统.CustomClassLoader@58372a00
自定义类加载器的父类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
------------------------
CustomClass 无参构造器执行
测试方法执行:自定义类加载器加载成功!
有参方法返回结果:Hello Java学习者,我是被自定义类加载器加载的类!
二、链接(Linking):验证 → 准备 → 解析
链接是将加载阶段生成的类元数据 与 JVM 运行时环境绑定的过程,核心目的是确保类的结构合法、可执行,为后续初始化和使用做准备。该阶段完全由 JVM 自动执行,无程序员干预,分为三个有序子阶段。
1. 验证(Verification):安全校验
核心目的
校验字节码文件是否符合JVM 规范,防止恶意字节码(如篡改魔数、非法指令、破坏语法规则)攻击 JVM,是 JVM 的安全屏障。
四层校验流程(由浅入深)
| 校验层级 | 核心校验内容 | 关键校验点 |
|---|---|---|
| 文件格式验证 | 校验字节码文件的基础格式合法性 | 魔数必须为0xCAFEBABE、版本号与当前 JVM 兼容、字节码长度合法 |
| 元数据验证 | 校验类的语义结构合法性 | 不能继承final类、不能实现私有接口、父类不能为自身、方法重写需要符合规则 |
| 字节码验证 | 校验指令执行逻辑的合法性 | 操作数栈与局部变量表匹配、跳转指令指向合法位置、避免空指针 / 数组越界等运行时异常 |
| 符号引用验证 | 校验常量池中符号引用的有效性 | 引用的类 / 方法 / 字段存在、权限符合访问规则(如不能访问私有成员) |
注:若校验失败,会抛出
VerifyError或其子异常(如ClassFormatError),类加载过程立即终止。
2. 准备(Preparation):静态变量内存分配与默认赋值
核心目的
为类的静态变量(类变量,static修饰) 在方法区(元空间) 分配内存,并为其设置JVM 默认初始值(非程序员在代码中指定的赋值)。
关键规则
- 仅处理静态变量,实例变量的内存分配在对象创建时(实例初始化阶段)完成,与本阶段无关;
- 默认初始值为 JVM 为每种数据类型定义的零值,与编程语言的默认值一致;
- 编译期 final 常量 (
static final且值为编译期可确定的字面量)是特例 ------ 本阶段直接赋值为程序员指定的常量值,而非零值。
典型示例(清晰对比默认值与最终值)
| 代码中静态变量定义 | 准备阶段赋值 | 初始化阶段赋值 | 赋值说明 |
|---|---|---|---|
public static int num = 10; |
0(int 零值) |
10 |
非 final 静态变量,遵循 "准备赋零值,初始化赋指定值" |
public static final int NUM = 20; |
20 |
20 |
编译期 final 常量,准备阶段直接赋最终值,初始化阶段无需重复赋值 |
public static Object obj; |
null(引用类型零值) |
程序员指定的对象 /null |
引用类型静态变量,零值固定为null |
public static final int RAND_NUM = new Random().nextInt(100); |
0 |
运行时随机数 | 运行期 final 常量(值编译期无法确定),按普通静态变量处理 |
3. 解析(Resolution):符号引用 → 直接引用
核心概念
- 符号引用 :常量池中以字符串形式 描述的类 / 方法 / 字段引用(如
java.lang.Object.toString()),仅描述目标名称,不关联实际内存地址; - 直接引用 :指向目标的实际内存地址 / 偏移量 / 句柄,是 JVM 运行时可直接使用的内存引用。
核心目的
将类的常量池中所有符号引用 替换为直接引用 ,建立类与依赖类、方法、字段之间的实际内存关联。
解析时机
JVM 采用懒加载策略,解析时机分为两种:
- 静态链接 :类初始化前完成解析(如静态方法、静态变量的引用);
- 动态链接 :类运行时(如调用多态方法)才完成解析(延迟解析),减少类加载时的性能开销。
注:解析阶段仅处理类 / 接口 / 方法 / 字段的引用,不处理实例对象的引用。
三、初始化(Initialization):执行静态代码逻辑
初始化是类加载过程中唯一真正执行程序员编写的 Java 代码 的阶段,核心作用是完成类的静态逻辑初始化,仅在类被主动引用 时触发,且仅执行一次。
1. 核心作用
执行 JVM 自动生成的类构造器<clinit>()方法,完成两件核心内容:
- 为静态变量赋程序员在代码中指定的显式值(覆盖准备阶段的默认值);
- 按源码顺序执行静态代码块(
static{}) 中的逻辑。
关于<clinit>()方法的关键特性
- 由 JVM自动生成 ,无需程序员手动定义,方法名是固定的
<clinit>; - 若类无静态变量显式赋值、无静态代码块,JVM不会生成
<clinit>()方法; - JVM 保证
<clinit>()方法的线程安全 :多个线程同时初始化一个类时,仅一个线程执行<clinit>(),其余线程阻塞等待,避免静态逻辑重复执行。
2. 触发初始化的条件:主动引用(7 种)
JVM 规范明确规定:只有对类进行「主动引用」时,才会触发类的初始化;反之,「被动引用」不会触发初始化,这是面试高频考点。
7 种主动引用场景(必记)
- 通过
new关键字创建类的实例(如new Parent()); - 访问 / 修改类的非 final 静态变量 (如
Parent.num = 20;、System.out.println(Parent.num);); - 调用类的静态方法 (如
Parent.testMethod();); - 通过
Class.forName("全限定类名")显式加载类(默认参数initialize=true,触发初始化); - 通过反射 API 操作类(如
Class.getDeclaredField()、Constructor.newInstance()); - 初始化子类时,若父类未初始化,则先触发父类的初始化;
- 执行程序入口类 (包含
main()方法的类),JVM 会首先初始化该类; - JDK7 + 新增:通过
java.lang.invoke.MethodHandle调用类的静态方法 / 访问静态变量。
3. 不触发初始化的条件:被动引用(3 个经典场景)
被动引用指仅通过类名访问类的相关资源,但未真正使用类的自身逻辑,JVM 判定为 "无需初始化",以下是 3 个最经典的被动引用场景,附字节码验证(基于 jclasslib)。
完整测试代码
java
package com.dwl.ex01_类加载子系统;
/**
* @Description 测试JVM类初始化的3个经典「不触发/仅部分触发」场景
* 遵循JVM规范:类仅在「主动使用」时触发初始化,「被动使用」不触发
* @Version 1.0.0
* @Date 2026/2/6 2:11
* @Author Dwl
*/
public class JvmClassInitTriggerRuleTest {
public static void main(String[] args) {
// 场景1:子类访问父类的普通静态变量 → 仅父类主动使用(初始化),子类被动使用(不初始化)
System.out.println("===== 场景1:子类访问父类普通静态变量 =====");
System.out.println("子类访问父类静态变量的值:" + ClassInitChild.parentNormalStaticVar);
System.out.println();
// 场景2:创建类的数组引用 → 仅生成数组类,原类(父类)被动使用,完全不触发初始化
System.out.println("===== 场景2:创建父类的数组引用 =====");
ClassInitParent[] parentClassArr = new ClassInitParent[10];
System.out.println("数组创建成功,数组的实际类型:" + parentClassArr.getClass().getName());
System.out.println();
// 场景3:访问类的编译期final常量 → 编译器内联常量值,原类(父类)被动使用,不触发初始化
System.out.println("===== 场景3:访问父类编译期final常量 =====");
System.out.println("访问父类编译期final常量的值:" + ClassInitParent.COMPILE_TIME_FINAL_CONST);
}
}
/**
* 父类 - 用于测试JVM类初始化触发规则
* 包含静态变量、编译期final常量、静态代码块(初始化标识)
*/
class ClassInitParent {
// 父类普通静态变量(非final,用于测试子类访问父类静态成员的场景)
public static int parentNormalStaticVar = 10;
// 父类编译期final常量(编译期可确定值,用于测试访问常量不触发初始化的场景)
public static final int COMPILE_TIME_FINAL_CONST = 20;
// 静态代码块:类初始化的核心标识(执行则说明类完成初始化)
static {
System.out.println("【ClassInitParent】父类执行初始化 <clinit> 方法");
}
}
/**
* 子类 - 继承父类,用于测试子类被动使用不触发自身初始化的场景
*/
class ClassInitChild extends ClassInitParent {
// 静态代码块:子类初始化标识(若未执行,说明子类未初始化)
static {
System.out.println("【ClassInitChild】子类执行初始化 <clinit> 方法");
}
}
运行结果
tex
===== 场景1:子类访问父类普通静态变量 =====
【ClassInitParent】父类执行初始化 <clinit> 方法
子类访问父类静态变量的值:10
===== 场景2:创建父类的数组引用 =====
数组创建成功,数组的实际类型:[Lcom.dwl.ex01_类加载子系统.ClassInitParent;
===== 场景3:访问父类编译期final常量 =====
访问父类编译期final常量的值:20
场景 1:子类访问父类普通静态变量 → 仅父类初始化,子类不初始化
核心结论 :ClassInitChild.parentNormalStaticVar本质是通过子类访问父类静态变量 ,子类仅为 "访问入口",属于被动引用 ,父类为主动引用。
字节码验证 :测试类的核心指令为getstatic #xx <ClassInitParent.parentNormalStaticVar : I>,指令直接指向父类 ,无任何子类相关的静态访问指令,因此仅触发父类<clinit>()执行,子类<clinit>()始终不执行。
场景 2:创建类的数组引用 → 原类完全不初始化
核心结论 :new ClassInitParent[10]并非创建ClassInitParent的实例,而是让 JVM动态生成一个数组类 (类名格式为[LClassInitParent;),原类仅作为 "数组元素类型标识",属于被动引用。
字节码验证 :核心指令为anewarray #xx <ClassInitParent>,该指令仅用于创建引用类型数组,无任何访问原类静态成员、调用原类<clinit>()的操作,因此原类完全不初始化。
场景 3:访问编译期 final 常量 → 原类不初始化
核心结论 :编译期 final 常量的值在准备阶段已确定 ,编译器会将常量值直接内联 到测试类的字节码中,测试类运行时无需访问原类,原类属于被动引用。
字节码验证 :核心指令为ldc #xx <20>,该指令直接从测试类自身的常量池 加载常量值20,无getstatic(访问静态变量)指令,因此原类完全不初始化。
关键区分:运行期 final 常量 (如
public static final int RAND_NUM = new Random().nextInt(100))编译期无法确定值,访问时会触发原类初始化,字节码指令为getstatic。



4. 继承体系下:类 + 实例初始化完整执行顺序
实际开发中,类的初始化常伴随实例化(new创建对象) ,且多存在继承关系,此时会同时触发类初始化(<clinit>()) 和实例初始化(<init>()) ,核心遵循 「静态先行、父类优先、源码顺序」 三大规则。
完整测试代码
java
package com.dwl.ex01_类加载子系统;
/**
* 测试继承体系下类初始化+实例初始化的完整执行顺序
* 核心:静态初始化(类级)→ 实例初始化(对象级),每级均遵循父类优先
* @Version 1.0.0
* @Date 2026/2/6
* @Author Dwl
*/
public class ClassInitInstanceInitOrderTest {
public static void main(String[] args) {
// 第一次new:触发类初始化+实例初始化
System.out.println("===== 第一次new子类实例 =====");
new InitOrderChild();
// 第二次new:仅触发实例初始化(静态内容仅执行一次)
System.out.println("\n===== 第二次new子类实例 =====");
new InitOrderChild();
}
}
/**
* 父类:含静态变量、静态代码块、实例变量、实例代码块、构造方法
*/
class InitOrderParent {
// 父类静态变量(类级)
public static int parentStaticVar = 10;
// 父类静态代码块(类级,<clinit>()一部分)
static {
System.out.println("1. 父类静态代码块");
}
// 父类实例变量(对象级)
public int parentInstanceVar = 20;
// 父类实例代码块(对象级,<init>()一部分)
{
System.out.println("3. 父类实例代码块");
}
// 父类无参构造方法(对象级,<init>()核心部分)
public InitOrderParent() {
System.out.println("4. 父类构造方法");
}
}
/**
* 子类:继承父类,结构与父类一致,验证执行顺序
*/
class InitOrderChild extends InitOrderParent {
// 子类静态变量(类级)
public static int childStaticVar = 30;
// 子类静态代码块(类级,<clinit>()一部分)
static {
System.out.println("2. 子类静态代码块");
}
// 子类实例变量(对象级)
public int childInstanceVar = 40;
// 子类实例代码块(对象级,<init>()一部分)
{
System.out.println("5. 子类实例代码块");
}
// 子类无参构造方法(对象级,<init>()核心部分)
public InitOrderChild() {
System.out.println("6. 子类构造方法");
}
}
运行结果
tex
===== 第一次new子类实例 =====
1. 父类静态代码块
2. 子类静态代码块
3. 父类实例代码块
4. 父类构造方法
5. 子类实例代码块
6. 子类构造方法
===== 第二次new子类实例 =====
3. 父类实例代码块
4. 父类构造方法
5. 子类实例代码块
6. 子类构造方法
5. 底层原理拆解:两大初始化阶段(JVM 层面)
执行new InitOrderChild()的完整流程,本质分为互不干扰、顺序执行 的两大阶段,所有执行顺序均由 JVM 强制规定,类初始化阶段一定先于实例初始化阶段执行。
阶段 1:类初始化阶段(类级,仅执行 1 次)
触发条件 :第一次对类进行主动引用(此处new InitOrderChild()是对子类的主动引用)。
核心执行 :JVM 自动生成的类构造器<clinit>()方法。
三大核心规则:
- 父类优先于子类:JVM 强制要求 "子类初始化前,必须先完成父类初始化",避免子类静态逻辑依赖父类静态变量但父类未初始化的问题;
- 按源码顺序执行 :JVM 将静态变量显式赋值语句 和静态代码块 按源码书写顺序 合并到
<clinit>()中,从上到下执行; - 仅执行 1 次 :类加载完成后,
<clinit>()方法被 JVM 缓存,后续无论new多少次实例,均不再执行。
本案例执行细节:
父类静态变量赋值(parentStaticVar=10) → 父类静态代码块(打印1) → 子类静态变量赋值(childStaticVar=30) → 子类静态代码块(打印2)。
阶段 2:实例初始化阶段(对象级,每次 new 都执行)
触发条件 :类初始化完成后,执行new指令创建实例。
核心执行 :JVM 根据手动编写的构造方法自动生成的实例构造器<init>()方法 (每个构造方法对应一个<init>())。
三大核心规则:
- 父类优先于子类 :JVM 会在子类
<init>()方法的最开头 自动添加super()指令(调用父类无参构造器),因此必须先完成父类实例初始化,再执行子类; - 按源码顺序执行 :JVM 将实例变量显式赋值语句 和实例代码块 按源码书写顺序 合并到每个
<init>()方法的开头,在构造方法体执行前执行; - 每次 new 都执行 :
<init>()方法与实例对象绑定,每次创建新对象都会重新执行,无缓存。
本案例执行细节:
tex
// 父类实例初始化(由super()触发)
父类实例变量赋值(parentInstanceVar=20)→ 父类实例代码块(打印3)→ 父类构造方法体(打印4)
// 子类实例初始化(父类执行完成后)
子类实例变量赋值(childInstanceVar=40)→ 子类实例代码块(打印5)→ 子类构造方法体(打印6)
6. jclasslib 字节码验证(关键指令)
通过 IDEA 的 jclasslib 插件可直观看到 JVM 生成的<clinit>()和<init>()方法,以及核心字节码指令,验证上述规则。
- 子类
<clinit>():无直接调用父类<clinit>()的指令,但 JVM 在执行子类<clinit>()前,会强制触发父类<clinit>()(JVM 层面规则,无需字节码); - 子类
<init>():第一行指令必为invokespecial #xx <InitOrderParent.<init> : ()V>,强制调用父类无参构造器; - 父类
<init>():指令顺序为bipush 20(实例变量赋值)→ 打印实例代码块 → 打印构造方法体,验证 "实例变量 + 代码块先于构造方法体执行"。



7. 拓展:带参构造与显式super()
若手动在子类构造方法中写super(xxx)(调用父类有参构造),仅需遵守一个规则,其余逻辑不变:
super(xxx)必须是子类构造方法的第一行代码 ,手动书写会覆盖 JVM 默认的super()(无参);- 实例初始化仍遵循 "父类优先",只是父类执行的是有参构造对应的
<init>()方法。
四、使用(Using)& 卸载(Unloading)
1. 使用(Using)
类完成初始化后,进入实际业务执行阶段 ,也是类的生命周期中持续时间最长的阶段。
核心操作:
-
通过
new关键字创建类的实例对象; -
调用类的静态方法 / 实例方法、访问静态变量 / 实例变量;
-
通过反射、动态代理等方式操作类或实例。
-
当类不再被使用时,会进入卸载阶段。
2. 卸载(Unloading)
类卸载是指 JVM 将类的元数据(方法区 / 元空间中) 和堆中的 Class 对象 全部回收的过程,是 JVM 的垃圾回收(GC)行为之一。类卸载的条件极其严格,缺一不可,否则会导致类元数据无法回收,引发内存泄漏。
类卸载的三个必要条件
- 该类的所有实例对象均已被 GC 回收,堆中无该类的任何实例;
- 加载该类的类加载器已被 GC 回收,无任何引用;
- 该类的Class 对象无任何引用(如反射代码中已释放对 Class 对象的引用,无静态变量引用该 Class 对象)。
关键注意点
- JVM核心类库 (如
java.lang.String、java.lang.Object)由启动类加载器(Bootstrap) 加载,启动类加载器是 JVM 的一部分,永远不会被回收,因此核心类库的类永远不会被卸载; - 自定义类加载器若使用不当(如静态持有类加载器实例、反射持有 Class 对象),极易导致类卸载失败,引发元空间内存泄漏;
- Tomcat 等容器通过自定义类加载器实现类的热部署:卸载旧的类加载器→加载新的类加载器,从而实现不重启容器更新类逻辑。
五、类加载核心生命周期流程图
类加载完整生命周期
加载(Loading)
读取外部字节码文件
方法区生成类元数据
堆区创建Class对象(唯一访问入口)
链接(Linking)
验证:四层安全校验,保证字节码合法
准备:静态变量分配内存+赋默认值
解析:符号引用转为直接内存引用
初始化(Initialization)
触发条件:类被主动引用
执行方法
静态变量显式赋值+静态代码块执行
使用(Using)
创建实例/调用方法/访问字段
卸载(Unloading)
满足三大条件,类元数据+Class对象被GC回收
六、核心总结
- 类加载的核心三阶段:加载(生成 Class 对象)→ 链接(验证 / 准备 / 解析)→ 初始化(执行静态逻辑),三个阶段按顺序执行,仅初始化阶段执行程序员编写的代码;
- 双亲委派模型的核心是 「父加载器优先」 ,目的是保证 JVM 内类的唯一性和核心类库的安全,自定义类加载器推荐重写
findClass()而非loadClass(); - 准备阶段为静态变量赋JVM 默认零值 ,初始化阶段赋程序员指定的显式值 ,编译期 final 常量在准备阶段直接赋最终值,是特例;
- 类初始化仅由主动引用触发 ,被动引用(子类访问父类静态变量、创建类数组、访问编译期 final 常量)不触发,
<clinit>()方法线程安全且仅执行一次; - 继承体系下的初始化规则:静态先行、父类优先、源码顺序 ,类初始化(
<clinit>())仅执行一次,实例初始化(<init>())每次 new 都执行; - 类卸载的关键是类加载器、Class 对象、类实例均无任何引用,启动类加载器加载的核心类库永远不会被卸载,自定义类加载器使用不当易引发元空间内存泄漏。
Java 类加载过程 核心记忆口诀
一、类加载五阶段(核心流程)
口诀:加验准解初,各有核心务;解析可延后,初始化执码
解释:
- 加:加载(获取字节码,生成 Class 对象,堆中存引用、元空间存类元信息);
- 验:验证(字节码 "安检",校验合法性 / 安全性,防止恶意篡改);
- 准:准备(类变量分配内存,仅赋 JVM 默认值,无代码执行);
- 解:解析(符号引用转直接引用,默认在初始化前,动态场景可延后);
- 初:初始化(唯一执行代码的阶段,执行
<clinit>(),赋程序员指定值)。
二、准备阶段 vs 初始化阶段(赋值核心区别)
口诀:准备赋默认,初始化赋指定;无码 vs 执码,阶段分前后
解释:
- 准备赋默认:仅给 static 类变量赋 JVM 默认值(int=0、boolean=false、引用 = null),仅分配内存,无任何代码执行;
- 初始化赋指定:执行
<clinit>()方法,给类变量赋代码中写的指定值,执行静态代码块; - 举例:
static int num = 20;→ 准备阶段 num=0,初始化阶段 num=20。
三、<clinit>() vs <init>()(构造器区别)
口诀:clinit 管类静,init 管实例;类初触发 clinit,new 出触发 init
解释:
- clinit:类构造器,处理静态变量 + 静态代码块,类初始化阶段触发,父类
<clinit>()先于子类执行; - init:实例构造器,处理实例变量 + 构造方法,new 创建对象时触发,先执行父类
<init>(); - 补充:无静态逻辑则不生成
<clinit>(),即使无自定义构造方法,JVM 也会生成默认<init>()。
四、主动使用 vs 被动使用(初始化触发)
口诀:主动用才初,被动只加载;常引父静数组列,皆为被动用
解释:
- 主动用:触发类初始化(new 对象、调静态 / 反射、子类初始化、主类执行、枚举使用);
- 被动用:仅加载类(不初始化),包括引用静态常量、子类用父类静态变量、创建类数组、
loadClass()加载类; - 核心:被动使用仅走 "加载 - 验证 - 准备"(部分到解析),不执行
<clinit>()。
五、Class.forName () vs ClassLoader.loadClass ()
口诀:forName 会初始化,loadClass 只加载;传 false 可控制,JDBC 用 forName
解释:
Class.forName("xxx"):默认触发初始化(执行<clinit>()),重载方法Class.forName("xxx", false, loader)可关闭初始化;ClassLoader.loadClass("xxx"):仅执行加载阶段,不触发初始化;- 场景:JDBC 加载驱动用
forName(),因需执行 Driver 类静态代码块完成驱动注册。
六、综合串讲口诀(整体记忆)
类加载五步走,加验准解初;
准备赋默认,初始化执码;
主动用才初,被动只加载;
clinit 管类静,init 管实例;
双亲父先求,加载只一次。