打破双亲委派#
一句话答案#
通过线程上下文类加载器(SPI 场景)、重写 loadClass()、OSGi/Tomcat 模块化来打破双亲委派。
核心要点
双亲委派模型回顾: 加载请求先委托给父加载器,直到 Bootstrap → 父加载器无法加载时,子加载器才自行加载。
为什么要打破?
- SPI 场景:核心类(JDK 8 在 rt.jar,JDK 9+ 在平台模块中)需要加载第三方实现(如 JDBC Driver)
- 热部署:同一个类需要加载不同版本(OSGi/Tomcat)
打破方式:
-
线程上下文类加载器(TCCL):
- SPI 机制:
ServiceLoader用 TCCL 加载接口实现类 - 例如:
DriverManager(JDK 8 由 Bootstrap 加载;JDK 9+ 所在的 java.sql 模块由 Platform ClassLoader 加载,同样看不到 classpath)通过 TCCL 加载具体的 MySQL Driver
- SPI 机制:
-
重写 loadClass():
- 默认 loadClass 实现了双亲委派逻辑
- 自定义类加载器可重写此方法改变委派顺序
-
OSGi/模块化:
- 每个 Bundle 有自己的 ClassLoader
- 网状加载(非树状),实现模块隔离和热部署
Tomcat 的类加载:
- 每个 WebApp 有独立的 WebAppClassLoader
- 先交 Bootstrap 加载 JVM 核心类(防止覆盖 java.*),然后优先自己加载(WEB-INF/classes、WEB-INF/lib),找不到再委托父加载器(System → Common)
- 实现了不同应用加载不同版本的同名类
面试回答(2分钟版)
双亲委派模型要求类加载请求先委托给父加载器,父加载器无法加载时才由子加载器自行加载,保证了核心类库的安全性。但某些场景下需要打破这个模型,主要有三种方式。第一种是线程上下文类加载器,最典型的场景是 SPI 机制。比如 JDBC 的 DriverManager 由 Bootstrap 类加载器加载(JDK 9+ 改由 Platform 类加载器加载,同样看不到 classpath),但具体的 MySQL Driver 在应用的 classpath 下,Bootstrap 加载不了,于是通过 Thread.currentThread().getContextClassLoader() 获取应用类加载器来加载驱动实现类,实现了父加载器借助子加载器完成加载。第二种是重写 loadClass 方法,因为双亲委派逻辑就实现在 loadClass 中,重写它就能改变委派顺序。第三种是模块化方案,比如 Tomcat 为每个 WebApp 创建独立的 WebAppClassLoader,除 JVM 核心类仍先交 Bootstrap 外,优先加载自己 WEB-INF/classes 和 WEB-INF/lib 下的类,找不到再委托父加载器,实现了不同应用加载不同版本同名类的隔离效果。区分 loadClass 和 findClass 很重要:loadClass 控制委派流程,findClass 控制实际加载逻辑,扩展用 findClass,打破用 loadClass。
追问与易错
追问方向:
- “JDBC 的 DriverManager 是怎么打破双亲委派加载驱动的?”→ TCCL 加载
- “SPI 的 ServiceLoader 原理?”→ 读取 META-INF/services 文件用 TCCL 加载实现类
- “Tomcat 中每个应用的类加载隔离怎么实现的?”→ 每个 WebApp 独立 ClassLoader
易错点:
- ❌ “自定义类加载器就是打破双亲委派”——只重写 findClass 不改变委派逻辑;重写 loadClass 改掉先委托父加载器的顺序才算打破
- ❌ 混淆 loadClass 和 findClass——loadClass 实现委派逻辑,findClass 实现加载逻辑