java类加载机制
类加载机制核心知识点总结
类的生命周期

包括以下 7 个阶段:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
| 阶段 | 说明 |
|---|---|
| 加载(Loading) | 读取 .class 文件,生成 Class 对象 |
| 验证(Verification) | 字节码格式验证、安全性验证 |
| 准备(Preparation) | 分配静态变量内存并初始化默认值 |
| 解析(Resolution) | 将符号引用转换为直接引用 |
| 初始化(Initialization) | 执行 <clinit> 方法,初始化静态变量和静态代码块 |
- 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。加载是类加载过程的第一步,主要完成下面 3 件事情:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的
Class对象,作为方法区这些数据的访问入口
- 每个 Java 类都有一个引用指向加载它的
ClassLoader。 数组类不是通过
ClassLoader创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。- 符号引用是 .class 文件中存储的一种间接引用方式,以字符串形式存在常量池中,并没有指向实际内存地址。比如类名,比如字段名
- 直接引用是类加载后由 JVM 解析得到的内存地址或偏移量。
- 其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
类初始化时机
1. 主动引用
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
2. 被动引用
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
- 通过子类引用父类的静态字段,不会导致子类初始化。
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
SuperClass[] sca = new SuperClass[10];
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
System.out.println(ConstClass.HELLOWORLD);
public final class UserConstants {
public static final int MAX_LOGIN_ATTEMPTS = 5;
public static final String ROLE_ADMIN = "ADMIN";
}
类加载器加载规则
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。
对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。
类加载器分类
JVM 中内置了三个重要的 ClassLoader:
BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库(%JAVA_HOME%/lib目录下的rt.jar、resources.jar、charsets.jar等 jar 包和类)以及被-Xbootclasspath参数指定的路径下的所有类。ExtensionClassLoader(扩展类加载器):主要负责加载%JRE_HOME%/lib/ext目录下的 jar 包和类以及被java.ext.dirs系统变量所指定的路径下的所有类。AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
🌈 拓展一下:
rt.jar:rt 代表“RunTime”,rt.jar是 Java 基础类库,包含 Java doc 里面看到的所有的类的类文件。也就是说,我们常用内置库java.xxx.*都在里面,比如java.util.*、java.io.*、java.nio.*、java.lang.*、java.sql.*、java.math.*。- Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说
java.base是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。
| 加载器 | 作用 |
|---|---|
| Bootstrap ClassLoader | 加载 JDK 核心类,如 java.lang.* |
| Extension ClassLoader | 加载 ext 目录下的扩展类 |
| Application ClassLoader | 加载 CLASSPATH 中的类 |
| 自定义 ClassLoader | 通过继承 ClassLoader 自定义逻辑 |
双亲委派模型(Parent Delegation)
加载请求会先委托给父加载器,层层向上,只有父类加载器无法加载时,才由当前加载器尝试加载。
双亲委派模型有什么好处?
- 避免重复加载
- 防止核心类被篡改(如你写了一个
java.lang.String) - 使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。

注意 ⚠️:双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。如果我们因为某些特殊需求想要打破双亲委派模型,也是可以的。 自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。
我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理。
其实这个双亲翻译的容易让别人误解,我们一般理解的双亲都是父母,这里的双亲更多地表达的是“父母这一辈”的人而已,并不是说真的有一个 MotherClassLoader 和一个FatherClassLoader 。个人觉得翻译成单亲委派模型更好一些,不过,国内既然翻译成了双亲委派模型并流传了,按照这个来也没问题,不要被误解了就好。
另外,类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。
JVM 判定两个 Java 类是否相同的具体规则:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。
类卸载
卸载类即该类的 Class 对象被 GC。
卸载类需要满足 3 个要求:
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC。ClassLoader GC才行
所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
只要想通一点就好了,JDK 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 JDK 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。
SPI
SPI(Service Provider Interface,服务提供接口)是 Java 提供的一种 服务发现机制,用于实现模块间的解耦,常用于框架底层或插件式架构中。
是文件的配置,就是定义了文件表示接口是由哪个实现类实现。
- 定义接口(服务接口)
- 实现接口(服务提供者)
- 在资源目录
/META-INF/services/下新建一个以接口全限定名命名的文件 - 文件内容是具体的实现类的全限定名
| 场景 | 例子 |
|---|---|
| JDBC 驱动注册 | DriverManager 通过 SPI 加载驱动类 |
| 日志框架适配 | SLF4J -> Logback、Log4j |
| Java 加密实现(JCE) | 加载不同加密算法 |
| Dubbo、Spring Boot 插件化 | 实现模块间解耦、插件注入 |
SPI 有什么缺点?
- 加载是全量的,不能懒加载
- 实现类无法按条件优先级排序
- 配置文件容易拼写出错,不易调试
- 类加载器隔离问题(如 OSGi、Tomcat)可能导致无法发现服务
改进方案
| 技术 | 优势 |
|---|---|
| Spring SPI | 提供条件过滤、排序、懒加载等功能(如 spring.factories)。 |
| Dubbo SPI | 增强 SPI,支持注解、扩展点自动激活 |
| Java 9 Module SPI | 基于 module-info.java 实现模块化 SPI |
Spring SPI = Spring 提供的一套 基于配置文件的接口发现与扩展机制。
核心思想:在 META-INF/spring.factories 文件中,配置接口与实现类映射,Spring 会在运行时自动加载。
QA
类是何时被加载进 JVM 的?
- 第一次主动使用时,比如:
- 创建对象(
new) - 访问静态变量、方法
- 反射调用
Class.forName() - 子类初始化会先触发父类初始化
- 创建对象(
类加载器之间的关系是继承还是组合?
是组合关系,
ClassLoader实例之间通过parent字段关联,不是继承树。
类加载调试与工具
- 查看类加载器:
getClass().getClassLoader() - 显示加载路径:
System.getProperty("java.class.path") jcmd,jmap,jstack:排查类加载问题- 使用
-verbose:class观察类加载过程
什么时候会破坏双亲委派模型?
- 自定义 ClassLoader 加载特定类(如 SPI 机制)
- Tomcat、Spring Boot 为了实现热部署,故意打破模型