1.3 类加载:双亲委派是妈宝行为吗?(手撕自定义类加载器)
▍ 类加载器的"家族企业"
想象一下,Java 的类加载器就像一个家族企业:
- Bootstrap 加载器:家族创始人,只负责核心业务(加载
java.lang等包) - Extension 加载器:家族二代,分管扩展业务(
jre/lib/ext目录) - Application 加载器:家族三代,负责日常业务(你的
classpath代码)
而 双亲委派 就是家族企业的规矩:
"儿子,这个活你能干吗?不能?那让爸爸来!"
(子加载器先让父加载器尝试加载类)
▍ 为什么需要"妈宝行为"?
用一个现实场景解释双亲委派的意义:
// 假如你自己写了个 java.lang.String
public class String {
public static void main(String[] args) {
System.out.println("我是黑客!");
}
}
如果没有双亲委派:
JVM 会加载你的 String 类,导致系统崩溃
有了双亲委派:
Application 加载器会先让爸爸(Bootstrap)加载官方 String
你的山寨 String 根本没机会执行!
▍ 手撕自定义类加载器
Step 1:创建测试类
// 文件路径:src/main/java/com/example/MagicClass.java
package com.example;
public class MagicClass {
static {
System.out.println("魔法类被加载!加载器是:"
+ MagicClass.class.getClassLoader());
}
}
Step 2:编译并生成 class 文件
javac MagicClass.java
# 将生成的 MagicClass.class 复制到项目根目录的 classes/com/example 下
Step 3:自定义类加载器
import java.nio.file.Files;
import java.nio.file.Paths;
public class CustomLoader extends ClassLoader {
// 指定类文件存储路径(比如:classes/com/example/MagicClass.class)
private final String classPath = "classes/";
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 1. 读取字节码文件
String path = name.replace('.', '/') + ".class";
byte[] bytes = Files.readAllBytes(Paths.get(classPath + path));
// 2. 定义类(关键方法)
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException("类没找到: " + name);
}
}
public static void main(String[] args) throws Exception {
CustomLoader loader = new CustomLoader();
// 用自定义加载器加载类
Class<?> magicClass = loader.loadClass("com.example.MagicClass");
// 创建实例(触发静态代码块)
Object instance = magicClass.getDeclaredConstructor().newInstance();
System.out.println("实例创建成功:" + instance);
}
}
运行结果:
魔法类被加载!加载器是:CustomLoader@1b6d3586
实例创建成功:com.example.MagicClass@4554617c
▍ 双亲委派流程图解
┌───────────────────┐
│ 自定义加载器 │
│ (CustomLoader) │
└─────────┬─────────┘
│ 1. 尝试加载类
▼
┌───────────────────┐
│ ApplicationClassLoader │
└─────────┬─────────┘
│ 2. 向上委派
▼
┌───────────────────┐
│ ExtensionClassLoader │
└─────────┬─────────┘
│ 3. 继续向上
▼
┌───────────────────┐
│ BootstrapClassLoader │
└─────────┬─────────┘
│ 4. 所有父加载器
│ 都加载失败
▼
┌───────────────────┐
│ 自定义加载器开始加载 │
│ (findClass()) │
└───────────────────┘
▍ 技术要点总结
-
打破双亲委派:
如果直接重写loadClass方法而不调用super.loadClass(),就能打破双亲委派
(但 99% 的情况下不要这么做!) -
沙箱安全机制:
自定义加载器加载的类无法直接访问核心库的包(如java.lang) -
热部署原理:
通过创建新的类加载器加载修改后的类,旧类会被 GC 回收
▍ 面试灵魂拷问
面试官:双亲委派有什么缺点?
你:就像家族企业容易僵化,某些场景需要"叛逆":
- Tomcat 需要隔离不同 Web 应用的类
- SPI 机制需要反向委派(如 JDBC 驱动加载)
这时候就要打破双亲委派!
(此时可以掏出手机展示刚写的自定义加载器代码)
"这是我实现的热部署 Demo,您要看看吗?"
▍ 课后彩蛋
尝试修改代码:
- 在
findClass方法中添加System.out.println("正在加载: " + name) - 观察加载
java.lang.String时会发生什么 - 思考为什么输出结果中没有打印核心类的加载信息
答案:Bootstrap 加载器是用 C++ 实现的,Java 层看不到它的加载过程!