类加载机制和双亲委派模型?

2025年 阅读约 9 分钟 面试指南 · Java面试 · JVM

深入解析JVM类加载机制:加载、验证、准备、解析、初始化五个阶段,双亲委派模型的工作原理和源码分析,打破双亲委派的三种场景(SPI、热部署、Tomcat),附完整面试模拟。

一句话总结

类加载分为加载 → 验证 → 准备 → 解析 → 初始化五个阶段。双亲委派模型要求类加载器收到加载请求时,先委托父加载器尝试加载,父加载器找不到才自己加载。这保证了 Java 核心类库(如 java.lang.String)由启动类加载器统一加载,防止被篡改。但 SPI 机制(如 JDBC)、Tomcat 类加载、OSGi 等场景需要打破双亲委派。

初级理解

类加载的五个阶段

.class 文件 → 加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载 ① 加载:通过全限定名获取类的二进制字节流,转为方法区的运行时数据结构,生成 Class 对象 ② 验证:确保 class 文件符合 JVM 规范(文件格式、元数据、字节码、符号引用验证) ③ 准备:为静态变量分配内存并赋零值(final static 直接赋初始值) ④ 解析:将常量池中的符号引用替换为直接引用 ⑤ 初始化:执行类构造器 <clinit>(),初始化静态变量和静态代码块

三类类加载器

类加载器加载路径说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib/rt.jarC++ 实现,无 Java 对象,加载核心类库
Extension ClassLoaderJAVA_HOME/jre/lib/ext/Java 实现,sun.misc.Launcher$ExtClassLoader
Application ClassLoaderclasspathJava 实现,加载用户类,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() 改变加载顺序。