【从零学习JVM|第五篇】打破双亲委派机制
目录
前言:
为什么要打破
打破双亲委派机制
自定义类加载器重写loadclass方法
线程上下文类加载器(以JDBC为例)
spi机制
核心角色与目标
实现流程
OSgi框架打破双亲委派机制
总结
前言:
双亲委派机制可以避免重复加载类,也可以保护核心类库的完整性和安全性,但是在实际开发中,我们还是会打破它,来达到我们的业务需求,接下来我们一起来探索,为什么要打破和怎么打破。
为什么要打破
- 实现模块化需求:在模块化系统中,不同模块可能需要加载不同版本的类库。例如 OSGi 框架是一个模块化框架,允许多个版本的相同类库共存,每个模块(Bundle)可能需要加载不同版本的类。传统的双亲委派模型无法满足这种需求,因为它会导致所有模块都使用由父加载器加载的同一个版本的类,而无法实现模块间类库版本的独立管理。
- 解决类冲突问题:在多应用或插件系统中,不同应用或插件可能需要加载相同名称但不同版本的类。以 Tomcat 为例,它可能部署多个 Web 应用,如果多个应用程序使用了不同版本的相同类库,遵循双亲委派模型就容易出错。因为双亲委派模型会优先使用父加载器加载的类,导致不同应用程序间的类无法隔离,容易产生冲突。而打破双亲委派模型,让每个 Web 应用有自己的类加载器,类加载器的顺序是 Common、Catalina、Shared、WebApp,通过调整类加载顺序,可确保 Web 应用能优先加载自己的类,避免类冲突。
- 支持动态加载和卸载:在需要动态加载和卸载类的场景中,如插件框架,传统的双亲委派模型会导致类加载器无法卸载已经加载的类。因为双亲委派模型下,类一旦被父加载器加载,就会被缓存,难以实现动态卸载。而打破双亲委派,通过自定义类加载器,可以实现类的动态加载和卸载,不影响其他插件。
- 满足热部署与热重载需求:在热部署和热重载场景中,如 Spring Boot DevTools 提供热部署功能,需要重新加载修改后的类。但传统的双亲委派模型会导致类加载器缓存问题,已加载的类不会被重新加载,无法及时更新类。打破双亲委派机制,可通过特殊的类加载器设计,如使用两个类加载器,一个用于加载不变的类,另一个用于加载可变的类,来实现类的热重载。
- 实现特殊业务需求:某些框架或工具(如某些 AOP 框架、容器技术)可能需要自定义类加载逻辑来增强功能。例如,一些 AOP 框架需要在类加载时进行字节码增强,以实现切面编程等功能,而双亲委派模型的标准加载流程可能不便于这种特殊的类加载逻辑实现,打破双亲委派可以让框架更灵活地控制类的加载过程,从而实现特定的业务功能。
打破双亲委派机制
自定义类加载器重写loadclass方法
我们一起来看在loadclass中双亲委派机制的代码
所以想要打破它我们就只需要把这段代码删除,自己重写loadclass方法。
import java.io.*;public class BreakingDelegateClassLoader extends ClassLoader {private final String classPath; // 类加载的根目录public BreakingDelegateClassLoader(String classPath, ClassLoader parent) {super(parent);this.classPath = classPath;}// 重写loadClass打破双亲委派@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {}// 实现自定义类查找@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {}
}
注意:如果我们单纯是想自己写一个类加载器,那我们只需要重写findclass方法即可,双亲委派的代码不要动。
线程上下文类加载器(以JDBC为例)
在JDBC驱动加载的场景中,Java核心库的DriverManager需要加载第三方厂商的JDBC驱动实现类。由于DriverManager位于rt.jar包中,由Bootstrap类加载器加载,而厂商的驱动实现类在应用classpath下,由AppClassLoader加载。这就不符合双亲委派机制的规定。
DriverManager是如何准确找到第三方的驱动的,它使用了spi机制。
spi机制
SPI(Service Provider Interface) 是一种软件设计模式,全称 “服务提供者接口”。它允许服务提供者(实现方)在不修改主程序的情况下,将自己的实现插入到系统中,实现框架与实现的解耦。其核心思想是:框架定义接口,第三方实现接口,通过约定的规则动态发现和加载实现类。
核心角色与目标
- 接口方:Java 定义 JDBC 标准接口(如
java.sql.Driver
),放在rt.jar
中,由顶层类加载器(BootstrapClassLoader
)加载。 - 实现方:数据库厂商(如 MySQL)提供接口的实现类(如
com.mysql.cj.jdbc.Driver
),放在驱动 jar 包中,由应用类加载器(AppClassLoader
)加载。 - 目标:让 Java 程序无需提前知道具体数据库驱动,动态找到并使用对应的驱动类。
实现流程
-
第一步:数据库厂商 “报名” 自己的驱动
-
在驱动 jar 包的
META-INF/services/
目录下,创建一个名为java.sql.Driver
的文件(和接口全限定名一致)。 -
文件里只写一行字:驱动实现类的完整路径(如
com.mysql.cj.jdbc.Driver
)。
-
-
第二步:Java 程序 “查名单” 找驱动
-
当程序需要连接数据库时(如
Class.forName("com.mysql.jdbc.Driver")
或直接使用DriverManager
),会触发ServiceLoader
工具类工作。 -
ServiceLoader
做的事很简单:-
去所有 jar 包的
META-INF/services/java.sql.Driver
文件里,读取所有 “报名” 的驱动类名。 -
用当前线程的类加载器(通常是应用类加载器)把这些类加载到内存。
-
-
-
第三步:驱动 “干活” 建立数据库连接
-
加载后的驱动类会自动注册到
DriverManager
中(驱动类的静态代码块会执行DriverManager.registerDriver(this)
)。 -
当程序调用
DriverManager.getConnection()
时,DriverManager
会遍历所有已注册的驱动,找到能处理目标数据库(如jdbc:mysql://
)的驱动,由它负责建立连接。
-
我们了解了DriverManager是通过spi机制找到了需要加载的驱动之后,又有一个问题,DriverManager是在rt.jar下的,那它是如何委托我们的应用程序类加载器的。采用了线程的上下文类加载器,我们可以来看看serviceloader的源码。
这段代码其实就是获取了加载器,然后使用迭代器进去加载器驱动,我们进入load方法。
可以看见cl就是通过线程获取的加载器,它就是我们的应用类加载器。
我们可以来总结一下JDBC的流程
- 启动类加载器加载DriverManager。
- 在初始化DriverManager的时候通过spi机制去找到要加载的驱动
- spi机制使用了线程上下文类加载器也就是应用程序类加载器来加载我们的驱动。
- 有人认为这种方式打破了双亲委派机制:因为这种由启动类加载器加载的类,委派了应用程序类加载器加载驱动,违背了双亲委派机制。
- 有人认为没有打破双亲委派:JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,驱动类加载就应该是应用程序类加载器来加载,遵循了双亲委派机制。
OSgi框架打破双亲委派机制
OSGi 框架打破双亲委派机制的核心在于重新定义了类加载的逻辑,以满足模块化系统中动态加载、版本隔离等需求。传统的双亲委派机制要求类加载器在加载类时先委托给父加载器处理,直到顶层加载器无法加载才由自身处理,这种机制在模块化场景下存在明显局限,比如无法让不同模块加载不同版本的同类,也难以实现模块间的灵活依赖管理。
OSGi 通过自定义类加载器(每个模块即 Bundle 都有独立的 BundleClassLoader)来颠覆这一规则。其类加载逻辑遵循 “本地优先” 原则:当 BundleClassLoader 尝试加载类时,会先检查当前 Bundle 的本地资源(如自身 jar 包中的类文件)、类缓存以及已导入的包,若找到类则直接加载,无需先委托给父加载器。只有当本地无法加载时,才会按规则向父加载器(通常是系统类加载器)委派。这种 “反向类加载” 机制打破了传统的 “父优先” 顺序,实现了模块的类加载自主权。
具体来说,OSGi 类加载器的工作流程如下:首先,BundleClassLoader 会优先从当前 Bundle 的 ClassPath 中查找类,包括检查是否已加载过该类(通过类名和加载器标识确保唯一性);若未找到,则查看当前 Bundle 导入的包(由其他 Bundle 导出的包),通过依赖关系到对应的 Bundle 中加载类;若仍未找到,才会委托给父加载器(系统类加载器)尝试加载 JDK 核心类或应用级公共类。这种机制使得不同 Bundle 可以加载同名但不同版本的类,因为它们的类加载器命名空间相互隔离,父加载器无法访问子加载器中的类,而子加载器可以按需访问父加载器中的类。
总结
打破双亲委派机制的核心优势在于突破传统类加载逻辑的限制,满足复杂场景下的类加载需求。其主要好处包括:实现类加载的灵活隔离,允许底层加载器的类被上层访问,支持同一 JVM 中不同版本同类共存;支持动态模块化系统,比如插件热部署或模块动态更新;解决类加载的循环依赖问题,避免因父加载器先加载类而导致的依赖冲突。
而打破双亲委派机制主要有三种方式:
其一,自定义类加载器。通过重写ClassLoader
的loadClass
方法,改变类加载顺序,先尝试本地加载而非优先委托父加载器。例如,Tomcat 的类加载器会先加载应用自身的类,再委托给父加载器,确保应用自定义类优先于容器或 JDK 类,实现 Web 应用间的类隔离。
其二,线程上下文类加载器(Thread Context ClassLoader)。Java 线程默认使用父线程的类加载器(通常是系统类加载器),但可通过Thread.setContextClassLoader
设置自定义加载器。典型场景如 JDBC 驱动加载:DriverManager 需要调用驱动类(由应用类加载器加载),但自身属于 JDK 类(由启动类加载器加载),传统双亲委派下父加载器无法访问子加载器的类,而通过线程上下文类加载器让 JDK 代码能反向使用应用类加载器加载驱动类,打破了单向委派的限制。
其三,OSGi 框架。每个模块(Bundle)拥有独立的类加载器,加载逻辑遵循 “本地优先” 原则:先检查当前 Bundle 的本地资源、导入包依赖,再委托给父加载器。这种机制允许不同 Bundle 加载不同版本的同类,通过导出 / 导入包管理类可见性,实现动态模块化部署。例如,BundleA 和 BundleB 可分别加载 Spring 4 和 Spring 5 的类,彼此隔离且互不干扰,同时支持模块的动态安装与更新,彻底颠覆了传统的双亲委派顺序。
总的来说学习双亲委派以及相关知识,可以让我们对java语言有一个更深入的理解。
感谢你的阅读,你的阅读和点赞是我最大的动力,创作不易,感谢支持。