
前言
这是最基础、最常用的场景 ------ 你写的每一行 Java 代码,运行时都会隐式触发类加载器工作,只是 JVM 帮你做了所有操作,你感知不到。
1. 程序启动时(最核心的默认场景)
当你执行 java Test 启动程序时,JVM 会立即触发类加载器:
- 首先加载
Test类(应用类加载器); Test类中引用的其他类(比如User、ArrayList),会在首次使用时被加载;- 核心类(如
String、Object)由启动类加载器加载,无需你干预。
例子:
java
public class Test {
public static void main(String[] args) {
// 首次使用User类,触发应用类加载器加载User.class
User user = new User();
// 首次使用ArrayList,触发启动类加载器加载java.util.ArrayList
List<String> list = new ArrayList<>();
}
}
本质:只要你使用一个类(创建实例、调用静态方法 / 字段、继承 / 实现),JVM 就会自动调用类加载器加载这个类。
2. 触发类加载的具体行为(JVM 规范定义)
JVM 规范明确了 6 种必须触发类加载的场景(称为 "主动使用"),只要满足其一,类加载器就会工作:
| 主动使用场景 | 例子 | 触发的类加载器 |
|---|---|---|
| 创建类的实例 | new User() |
应用类加载器 |
| 调用类的静态方法 | User.staticMethod() |
应用类加载器 |
| 访问类的静态字段(非 final) | System.out.println(User.staticField) |
应用类加载器 |
| 反射调用类 | Class.forName("User") |
应用类加载器 |
| 初始化子类 | 子类Student extends User,加载 Student 时先加载 User |
应用类加载器 |
| 启动包含 main 方法的主类 | java Test |
应用类加载器 |
补充:
final静态常量(如public static final int NUM = 100)是编译期常量,直接存入运行时常量池,不会触发类加载。
简单来说:你写的 Java 代码能运行,第一步就是类加载器把对应的.class 文件加载到内存;而当你需要灵活控制类的加载规则时,就需要主动使用类加载器。
一、明确类加载器的核心工作
类加载器的核心工作就是把硬盘上的.class文件(字节码)加载到 JVM 内存中,并生成对应的Class对象,让程序能使用这个类。我会用通俗的场景 + 代码例子,帮你彻底看懂类加载器到底在做什么。
类加载器就像 "JVM 的文件搬运工":
- 从指定位置(硬盘、网络、内存等)找到编译好的
.class文件; - 把
.class文件的字节码读取到 JVM 内存中; - 将字节码转换为 JVM 能识别的
java.lang.Class对象(这个对象是类的 "元数据模板",程序通过它创建实例); - 同时负责类的唯一性校验(同一个类被不同类加载器加载,会被 JVM 视为不同的类)。
二、用生活场景举例:类比理解类加载器
假设你是一家公司的员工(JVM),需要用一份 "员工手册"(Java 类,比如User.class):
.class文件 = 打印好的纸质手册(存在公司文件柜里);- 类加载器 = 行政助理(专门负责找手册、复印、送到你手上);
Class对象 = 你手上的手册复印件(你能直接看、用,对应程序能通过Class对象创建实例);- 核心动作:行政助理(类加载器)从文件柜(硬盘)找到手册(
.class)→ 复印(加载到内存)→ 给你(生成Class对象)→ 你用手册做事(程序用Class对象创建User实例)。
三、代码例子:直观看到类加载器的工作过程
我们通过代码,一步步看类加载器如何加载类、生成Class对象:
步骤 1:编写一个简单的 Java 类(生成.class文件)
java
// User.java
public class User {
private String name;
public User(String name) {
this.name = name;
}
public void sayHello() {
System.out.println("你好,我是" + name);
}
}
执行javac User.java,生成User.class文件(存放在硬盘上,比如/Users/xxx/目录下)。
步骤 2:用代码展示类加载器的加载过程
java
// ClassLoaderDemo.java
public class ClassLoaderDemo {
public static void main(String[] args) throws Exception {
// 1. 获取系统类加载器(默认加载我们自己写的类)
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("默认类加载器:" + systemClassLoader.getClass().getName());
// 2. 类加载器的核心工作:加载User.class,生成Class对象
// 这里的"User"是类的全限定名(如果有包,要写包名+类名,比如com.example.User)
Class<?> userClass = systemClassLoader.loadClass("User");
// 3. 验证:加载后生成了Class对象(这就是类加载器的核心产物)
System.out.println("加载后生成的Class对象:" + userClass);
System.out.println("该Class对象的类加载器:" + userClass.getClassLoader().getClass().getName());
// 4. 通过Class对象创建实例(类加载的最终目的)
User user = (User) userClass.getConstructor(String.class).newInstance("Java学习者");
user.sayHello(); // 调用方法,验证类加载成功
}
}
步骤 3:运行结果(关键解读)
默认类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader
加载后生成的Class对象:class User
该Class对象的类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader
你好,我是Java学习者
代码关键解读(类加载器的工作细节)
ClassLoader.getSystemClassLoader():获取 JVM 默认的 "应用类加载器"(负责加载我们自己写的类,比如User);loadClass("User"):类加载器的核心方法,做了 3 件事:- 查找:在指定路径(当前目录)找到
User.class文件; - 读取:把
User.class的字节码读取到 JVM 内存; - 转换:将字节码转换为
Class<User>对象,存入方法区(元空间);
- 查找:在指定路径(当前目录)找到
- 通过
Class对象创建实例 :类加载器加载类的最终目的,是让程序能通过Class对象创建实例、调用方法 ------ 如果没有类加载器,JVM 根本不知道User类的存在,更无法使用它。
四、扩展:不同类加载器的分工(理解类加载器的设计)
JVM 内置了 3 类核心类加载器,分工明确(就像公司不同级别的行政):
| 类加载器类型 | 核心工作(加载范围) | 类比场景 |
|---|---|---|
| 启动类加载器(Bootstrap) | 加载 JDK 核心类(如java.lang.String、java.util.ArrayList),存放在rt.jar中 |
公司总部的行政,负责核心制度文件 |
| 扩展类加载器(Extension) | 加载 JDK 扩展类(如javax.开头的类),存放在jre/lib/ext目录 |
分公司行政,负责扩展制度文件 |
| 应用类加载器(Application) | 加载我们自己写的类、第三方 jar 包(如 Spring、MyBatis) | 部门行政,负责部门专属文件 |
(注:启动类加载器是 JVM 底层 C++ 实现,不是 Java 类,所以getClassLoader()返回null)
总结
- 类加载器的核心工作:找到
.class文件 → 读取字节码到内存 → 生成Class对象,让 JVM 能识别并使用这个类; - 类加载器不是单一的,而是分工明确的 "加载器体系"(启动 / 扩展 / 应用类加载器);
- 最终目的:生成
Class对象,程序通过这个对象创建实例、调用方法 ------ 没有类加载器,所有 Java 类都只是硬盘上的.class文件,无法被 JVM 执行。
你的核心理解可以简化为:类加载器是 "连接硬盘.class文件和 JVM 内存的桥梁",没有它,Java 代码编译后的字节码永远无法被运行。
五、双亲委派的核心定义
双亲委派模型 是 JVM 类加载器加载类时遵循的 "向上委托、向下查找" 规则:当一个类加载器收到加载类的请求时,它不会自己先加载,而是把请求委托给它的 "父类加载器" 去完成;只有当父类加载器无法加载(找不到对应的.class 文件)时,子加载器才会自己尝试加载。
注意:这里的 "父类加载器" 不是 Java 继承关系的父类,而是逻辑上的父子关系(比如应用类加载器的父加载器是扩展类加载器,扩展类加载器的父加载器是启动类加载器)。
用生活场景类比:理解双亲委派的逻辑
假设你(应用类加载器)在公司要找一份文件(加载 User 类):
- 你先把找文件的请求交给你的直属领导(扩展类加载器);
- 领导又把请求交给公司老板(启动类加载器);
- 老板先找:如果老板有这份文件(比如是 JDK 核心类),直接给你,流程结束;
- 如果老板没有,领导再找:领导有就给你,没有则流程到你;
- 最后你自己找:你找到自己的文件(User 类),交给使用方。
这个 "先找上级、上级找不到自己再找" 的逻辑,就是双亲委派的核心。
六、理解双亲委派的执行过程
我们以加载User类(自己写的类)和String类(JDK 核心类)为例,拆解双亲委派的执行步骤:
1. 先明确类加载器的父子关系(从上到下)
启动类加载器(Bootstrap)← 扩展类加载器(Extension)← 应用类加载器(Application)
(箭头表示 "父级",应用类加载器是最底层的子加载器)
2. 加载 JDK 核心类(String)的流程(双亲委派的 "上级处理")
当程序需要加载java.lang.String时:

- 结果:String 类由启动类加载器加载,应用类加载器全程只是 "转发请求"。
3. 加载自定义类(User)的流程(双亲委派的 "自己处理")
当程序需要加载User类时:

- 结果:只有父加载器都加载失败时,应用类加载器才自己加载 User 类。
注:JDK 9 + 把扩展类加载器改名为平台类加载器(PlatformClassLoader),逻辑不变。
七、双亲委派的核心作用(为什么要设计这个规则?)
- 保证核心类的唯一性 :避免自定义类覆盖 JDK 核心类(比如你自己写一个
java.lang.String类,双亲委派会让启动类加载器先加载 JDK 的 String,你的自定义 String 永远不会被加载,防止核心类被篡改); - 保证类加载的安全性 :核心类只能由启动类加载器加载,避免恶意代码替换核心类(比如替换
java.lang.Object); - 优化类加载效率:核心类只需要启动类加载器加载一次,所有子加载器都能复用,无需重复加载。
八、反例:打破双亲委派(了解即可)
有些场景需要打破双亲委派(比如 Tomcat 的类加载器):Tomcat 需要为不同 Web 应用加载各自的类,即使类名相同,也要视为不同类,因此 Tomcat 的类加载器会先自己加载,加载不到再委托父加载器(反向委派)。但这是特殊场景,JVM 默认遵循双亲委派。
九、打破双亲委派核心方法
方法 1:重写 ClassLoader 的 loadClass () 方法(最核心、最常用)
JVM 默认的ClassLoader.loadClass()方法是双亲委派的核心实现,源码逻辑简化如下:
java
// 父类ClassLoader的默认loadClass逻辑(双亲委派)
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 先检查该类是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 2. 委托父加载器加载(核心:先找父类)
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 父加载器是启动类加载器,直接找核心类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器加载失败,抛出异常
}
if (c == null) {
// 3. 父加载器失败,自己加载(findClass是子类实现)
long t1 = System.nanoTime();
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
打破方式 :重写loadClass()方法,调换 "委托父加载器" 和 "自己加载" 的顺序 ------ 先自己加载,加载失败再委托父加载器。
java
// 自定义类加载器:打破双亲委派
public class BreakParentDelegationClassLoader extends ClassLoader {
private String rootPath; // 自定义类加载路径
public BreakParentDelegationClassLoader(String rootPath, ClassLoader parent) {
super(parent); // 指定父加载器
this.rootPath = rootPath;
}
// 重写loadClass方法,打破双亲委派
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查类是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
// 2. 先自己加载(打破核心:优先自己,而非父加载器)
try {
c = findClass(name); // 自己加载自定义路径下的类
} catch (ClassNotFoundException e) {
// 3. 自己加载失败,再委托父加载器
if (getParent() != null) {
c = getParent().loadClass(name);
} else {
c = findBootstrapClassOrNull(name);
}
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
// 实现findClass:加载自定义路径下的.class文件
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 类名转文件路径:com.example.User → rootPath/com/example/User.class
String filePath = rootPath + name.replace(".", "/") + ".class";
byte[] classBytes = Files.readAllBytes(Paths.get(filePath));
// 把字节码转为Class对象
return defineClass(name, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("类加载失败:" + name, e);
}
}
// 测试:加载自定义路径下的类,优先使用自己的加载器
public static void main(String[] args) throws Exception {
// 自定义类加载路径(比如D:/custom_classes/)
String customPath = "D:/custom_classes/";
// 创建自定义加载器,父加载器为应用类加载器
BreakParentDelegationClassLoader customLoader = new BreakParentDelegationClassLoader(customPath, ClassLoader.getSystemClassLoader());
// 加载自定义路径下的com.example.User类
Class<?> userClass = customLoader.loadClass("com.example.User");
System.out.println("类加载器:" + userClass.getClassLoader().getClass().getName());
// 验证:即使父加载器能找到该类,也会优先用自定义加载器加载
}
}
核心说明:
- 重写
loadClass()后,优先执行findClass()(自己加载),失败后才委托父加载器; - 这是 Tomcat 类加载器的核心实现逻辑(Tomcat 为每个 Web 应用创建独立加载器,优先加载应用内的类,避免和其他应用 / 核心类冲突)。
总结
- 双亲委派的核心规则:先委托父加载器加载,父加载器失败后子加载器才自己加载;
- 核心目的:保证 JDK 核心类的唯一性和安全性,避免核心类被篡改或重复加载;
- 执行流程:应用类加载器 → 扩展类加载器 → 启动类加载器(向上委托),加载失败则反向向下查找。
记住这个核心逻辑:双亲委派是 "先找上级,上级不行自己来",本质是为了保护 JVM 核心类的安全和唯一性。