📦 类加载机制
类加载五大阶段 · 双亲委派模型 · 破坏双亲委派 · 自定义类加载器
1. 类加载的完整过程?(五阶段)
- 加载(Loading):通过全限定名获取二进制字节流(class 文件/网络/jar),转化为方法区的运行时数据结构,堆中生成 java.lang.Class 对象作为入口
- 验证(Verification):文件格式、元数据、字节码语义验证,保证不会危害 JVM(ClassFormatError/VerifyError 都在这里)
- 准备(Preparation):为静态变量分配内存并赋零值(注意:是零值,不是代码里的初始值!
static int a = 10此时 a=0;真正的 10 在初始化阶段赋) - 解析(Resolution):常量池的符号引用 → 直接引用(定位到实际内存地址)。解析可延迟到首次使用
- 初始化(Initialization):执行
<clinit>()(静态变量赋值 + 静态代码块)。只有主动使用类才会触发初始化
什么时候会触发初始化(主动使用):new/getstatic/putstatic/invokestatic、反射、初始化子类前先初始化父类、main 类、JDK7 动态语言支持。被动使用不触发:通过子类引用父类静态字段(只初始化父类)、定义数组引用、final 常量(编译期已入常量池)。
🎯 面试要点
- 准备阶段赋零值 vs 初始化赋真实值的区分是经典考点
- 初始化是线程安全的:JVM 保证 clinit 加锁执行,多线程同时触发只执行一次
- 加载、验证、准备、初始化的顺序是确定的,解析可推迟
2. 什么是双亲委派模型?为什么要这么设计?
三个内置类加载器:
- 启动类加载器(Bootstrap):加载
rt.jar(JDK9+ 为 jmods/平台模块),C++ 实现,无父加载器 - 扩展类加载器(Extension):JDK 8 加载
<java.home>/lib/ext;JDK 9 起该目录已不存在,改为 Platform ClassLoader,加载的是平台模块(java.sql、java.xml 等) - 应用程序类加载器(Application):加载 classpath,也就是我们自己写的类
工作流程:一个类加载请求,先委派给父加载器,父加载器逐级向上,只有父加载器加载不了(找不到类)才回落到子加载器自己加载。
设计目的:
- 避免重复加载:类只被加载一次
- 安全性:核心类必须由 Bootstrap 加载。防止你自己写一个 java.lang.String 替换 JDK 的 String(自定义 String 无法加载,因为请求委派到 Bootstrap,它找不到就抛,不会让应用加载器加载同名核心类)
源码核心:loadClass 递归委派
protected Class<?> loadClass(String name) {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name); // 1. 已加载过?
if (c == null) {
try {
if (parent != null)
c = parent.loadClass(name); // 2. 先让父加载器加载
else
c = findBootstrapClassOrNull(name);
} catch (ClassNotFoundException e) {
// 父加载器抛异常说明它加载不了
}
if (c == null)
c = findClass(name); // 3. 父加载不了,自己加载
}
return c;
}
}
🎯 面试要点
- 类相等判断:同一个类,不同加载器加载 = 两个不同的 Class(可用 ObjectInputStream 序列化报 ClassCastException 引出)
- 判断类是否由同一加载器加载:obj.getClass().getClassLoader()
- JDK9+ 模块化后内置加载器层级仍在,只是扩展类加载器变为平台类加载器
3. 什么场景会破坏双亲委派?
双亲委派要求"核心类不被应用类加载",但框架需要反向使用应用类加载器加载自己的实现类,于是出现了三次破坏:
- 第一次:JDBC(SPI 机制)。DriverManager 在 rt.jar 中由 Bootstrap 加载,但 MySQL 驱动在 classpath。JDBC 用 Thread.currentThread().getContextClassLoader()(线程上下文类加载器)加载驱动实现——变相破坏"只向上委派"。
- 第二次:Tomcat 容器。每个 WebApp 需要隔离(两个应用可以加载不同版本的 Spring)+ 共享基础类。Tomcat 自定义 WebAppClassLoader 先加载 WEB-INF/classes,找不到再向上委派——顺序反转。
- 第三次:OSGi / 热部署。追求模块化和动态替换,类加载器网络化,没有父子关系。
注意:SPI 不是推翻双亲委派,而是补充了一条"由内向外"的加载路径。
🎯 面试要点
- JDBC 的 4 步驱动加载流程:DriverManager 初始化 → 加载 META-INF/services → ServiceLoader → 线程上下文类加载器
- Tomcat 的 WebAppClassLoader 先自己找 WEB-INF/classes、WEB-INF/lib(JRE 核心类除外),找不到才向上委派——顺序与标准双亲委派相反
- Arthas 能挂到进程上靠的是动态修改类(Instrumentation),与类加载顺序无关
4. 如何自定义类加载器?典型应用场景?
继承 ClassLoader,重写 findClass()(JDK 推荐,保持双亲委派)或 loadClass()(完全接管)。核心就是读取字节码 → defineClass() → 返回 Class。
从自定义路径加载 class
class DiskClassLoader extends ClassLoader {
private final String path;
DiskClassLoader(String path, ClassLoader parent) {
super(parent);
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = Files.readAllBytes(
Path.of(path, name.replace('.', '/') + ".class"));
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}
应用场景:热部署(改代码不重启,重新 new 一个加载器加载新版本 class)、加密 class 文件(解密后再 defineClass)、模块隔离(不同插件不同加载器)。
🎯 面试要点
- 重写 findClass 保持双亲委派;重写 loadClass 才会破坏委派
- 热部署的代价:旧类加载器无法卸载 → 频繁热部署会 Metaspace 泄漏(JDK8 前是永久代泄漏)
- 父子加载器的类可见性:子可见父的类,父不可见子的类