一句话总结
类加载分为加载 → 验证 → 准备 → 解析 → 初始化五个阶段。双亲委派模型要求类加载器收到加载请求时,先委托父加载器尝试加载,父加载器找不到才自己加载。这保证了 Java 核心类库(如 java.lang.String)由启动类加载器统一加载,防止被篡改。但 SPI 机制(如 JDBC)、Tomcat 类加载、OSGi 等场景需要打破双亲委派。
初级理解
类加载的五个阶段
.class 文件 → 加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
① 加载:通过全限定名获取类的二进制字节流,转为方法区的运行时数据结构,生成 Class 对象
② 验证:确保 class 文件符合 JVM 规范(文件格式、元数据、字节码、符号引用验证)
③ 准备:为静态变量分配内存并赋零值(final static 直接赋初始值)
④ 解析:将常量池中的符号引用替换为直接引用
⑤ 初始化:执行类构造器 <clinit>(),初始化静态变量和静态代码块
三类类加载器
| 类加载器 | 加载路径 | 说明 |
| Bootstrap ClassLoader | JAVA_HOME/jre/lib/rt.jar | C++ 实现,无 Java 对象,加载核心类库 |
| Extension ClassLoader | JAVA_HOME/jre/lib/ext/ | Java 实现,sun.misc.Launcher$ExtClassLoader |
| Application ClassLoader | classpath | Java 实现,加载用户类,getSystemClassLoader() |
双亲委派模型
// ClassLoader.loadClass() 源码(简化)
protected Class<?> loadClass(String name, boolean resolve) {
synchronized (getClassLoadingLock(name)) {
// ① 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// ② 委托父加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// ③ 父加载器为 null → 使用 Bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) { }
if (c == null) {
// ④ 父加载器找不到 → 自己加载
c = findClass(name);
}
}
if (resolve) resolveClass(c);
return c;
}
}
中级深入
双亲委派的好处
| 好处 | 说明 |
| 避免重复加载 | 父加载器加载过的类,子加载器不再加载 |
| 安全保护 | 防止核心 API 被篡改,如自定义 java.lang.String 不会被加载 |
| 类的唯一性 | 同一个类 + 同一个类加载器 = 同一个 Class 对象 |
准备阶段的零值
public class Demo {
public static int a = 1; // 准备阶段:a = 0,初始化阶段:a = 1
public static final int b = 2; // 准备阶段:b = 2(final static 直接赋值)
public static String s = "hi"; // 准备阶段:s = null,初始化阶段:s = "hi"
}
触发初始化的六种情况
// ① new 对象、读取/设置静态字段(final 除外)、调用静态方法
new Demo();
Demo.staticField = 1;
// ② 反射调用
Class.forName("com.example.Demo");
// ③ 初始化子类时,父类必须先初始化
class Child extends Parent {} // new Child() 先初始化 Parent
// ④ 虚拟机启动时,包含 main() 的主类
// ⑤ MethodHandle 解析结果为 REF_getStatic 等
// ⑥ 接口的 default 方法被实现类继承时
高级拓展
打破双亲委派的三种场景
场景一:SPI 机制(JDBC 驱动加载)
// JDBC 的 DriverManager 在 rt.jar 中,由 Bootstrap 加载
// 但具体的驱动实现(如 mysql-connector)在 classpath 中
// Bootstrap 无法加载 classpath 中的类!
// 解决:线程上下文类加载器(Thread Context ClassLoader)
// DriverManager 通过 Thread.currentThread().getContextClassLoader()
// 获取 Application ClassLoader 来加载驱动实现类
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
// 内部使用线程上下文类加载器打破双亲委派
场景二:Tomcat 类加载
// Tomcat 的类加载器层次(违反双亲委派):
// Bootstrap
// └── System(Application)
// └── Common(加载 Tomcat 和所有 Web 应用共享的类)
// ├── Catalina(加载 Tomcat 自身类)
// ├── Shared(加载所有 Web 应用共享的类)
// └── Webapp(每个 Web 应用独立,优先自己加载)
// WebappClassLoader 优先自己加载,找不到才委托父加载器
// 目的:1) 不同 Web 应用的类隔离 2) 应用可覆盖父加载器的类
场景三:OSGi 模块化
OSGi 使用网状类加载结构,每个 Bundle 有自己的类加载器,通过 Import/Export Package 声明依赖关系,完全打破了双亲委派的树状结构。
自定义类加载器
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 从自定义路径读取 .class 字节码
byte[] bytes = loadClassData(name);
// 调用 defineClass 将字节码转为 Class 对象
return defineClass(name, bytes, 0, bytes.length);
}
private byte[] loadClassData(String name) {
// 从文件系统、网络、数据库等加载字节码
}
}
实战场景
场景一:验证双亲委派
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader cl = String.class.getClassLoader();
System.out.println(cl); // null → Bootstrap ClassLoader(C++实现)
ClassLoader appCl = ClassLoaderTest.class.getClassLoader();
System.out.println(appCl); // sun.misc.Launcher$AppClassLoader
// 打印类加载器层次
ClassLoader current = appCl;
while (current != null) {
System.out.println(current);
current = current.getParent();
}
// AppClassLoader → ExtClassLoader → null(Bootstrap)
}
}
场景二:自定义 java.lang.String 能否加载?
// 自己写一个 java.lang.String 类
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("自定义 String");
}
}
// 运行结果:
// 错误: 在类 java.lang.String 中找不到 main 方法
// 原因:双亲委派,Bootstrap 先加载了 rt.jar 中的 String
// 自定义的 String 根本没有机会被加载
面试模拟
Q:类加载的过程是怎样的?
A:五个阶段:加载(获取字节流→方法区→Class对象)、验证(文件格式/元数据/字节码/符号引用验证)、准备(静态变量赋零值,final static 直接赋值)、解析(符号引用→直接引用)、初始化(执行 <clinit>(),初始化静态变量和静态代码块)。
Q:什么是双亲委派模型?为什么要这样设计?
A:类加载器收到加载请求时,先委托父加载器加载,父加载器找不到才自己加载。好处:1) 避免重复加载,父加载器加载过的类不再加载;2) 安全保护,核心类库如 java.lang.String 由 Bootstrap 统一加载,防止被篡改;3) 类的唯一性,同一个类+同一个类加载器=同一个 Class 对象。
Q:什么场景需要打破双亲委派?怎么打破?
A:1) SPI 机制:核心类库(Bootstrap加载)需要调用用户实现类(AppClassLoader加载),通过线程上下文类加载器打破;2) Tomcat:WebappClassLoader 优先自己加载,实现应用隔离和版本覆盖;3) OSGi:网状类加载结构。打破方式:重写 loadClass() 或 findClass() 改变加载顺序。