打破双亲委派模型
在 Java 中,双亲委派模型是默认的类加载机制,旨在保证类加载的安全性和一致性。
打破双亲委派模型,指允许自定义类加载器直接加载类,而不再优先委托给父类加载器。
打破双亲委派模型的主要场景通常出现在特殊框架(如应用服务器、插件系统)中,或者是为了实现热部署、模块隔离等高级功能。
1. 应用服务器或 Web 容器(如 Tomcat)
背景:
Java 的应用服务器(如 Tomcat、Jetty)或 Java EE 容器通常需要运行多个 Web 应用(每个应用有自己独立的类库)。这些类库可能会包含和 Java 核心类库相同的类名或版本不同的类。
为什么需要打破双亲委派?
假设 Web 应用中有一个
javax.servlet.Servlet
接口。在标准的双亲委派模型下,这个类会被父类加载器(系统类加载器)加载。如果不同版本的 Web 应用使用了不同的Servlet
实现,父类加载器总是先加载同一个版本的类,可能导致类冲突。解决这个问题的方法是:每个 Web 应用都有自己的类加载器。通过自定义类加载器,Tomcat 可以直接为 Web 应用加载它自己的类,而不总是委托给父类加载器。这种方式实现了类隔离,允许不同应用使用不同版本的类。
示例:
Tomcat 中,每个 Web 应用都有自己的类加载器(WebappClassLoader
)。当你部署一个 Web 应用时,Tomcat 会为这个应用创建一个独立的类加载器,确保应用的类不会与全局的类库产生冲突。
- Bootstrap 类加载器
|
- System 类加载器
|
- WebappClassLoader (为每个 Web 应用创建)
打破双亲委派的方式允许 Tomcat 的 Web 应用加载自己特定版本的类,而不是系统默认的类库。
2. OSGi(动态模块化系统)
背景:
OSGi 是 Java 中实现模块化的框架,它允许应用程序在运行时加载、卸载、更新模块(称为 bundle)。这些模块可以独立加载自己的类,并且不同模块之间可能会有版本冲突。
为什么需要打破双亲委派?
在 OSGi 中,不同的模块可能会依赖不同版本的类库。如果按照标准的双亲委派模型,父类加载器会优先加载类,导致所有模块共享同一个类库版本。这种方式不适合 OSGi 的场景,因为 OSGi 允许模块之间有不同的依赖版本。
OSGi 通过自定义的类加载机制打破了双亲委派模型,使得每个模块有自己独立的类加载器。这样,不同模块可以加载相同类的不同版本,解决了类版本冲突的问题。
示例:
在 OSGi 中,每个 bundle 有自己的类加载器,通过自定义的类加载器,可以直接加载模块内的类,而不必委托给父类加载器。
- Bootstrap 类加载器
|
- OSGi Bundle 类加载器(为每个模块创建)
这种方式允许每个模块加载自己的依赖,而不受全局类库的影响。
3. JDBC Driver 加载
背景:
Java 应用程序通常需要使用数据库驱动程序(JDBC Driver)来连接数据库。JDBC 驱动程序是由第三方厂商提供的,通常放在应用的 classpath
中。
为什么需要打破双亲委派?
JDBC 驱动程序通常是由应用类加载器加载的,但 JDBC 连接的调用是在核心类库中(如
java.sql.DriverManager
),这些类是由 Bootstrap 类加载器加载的。如果严格按照双亲委派模型,应用类加载器加载的 JDBC 驱动无法被核心类库直接使用。为了解决这个问题,JVM 中的
DriverManager
使用了一种线程上下文类加载器的机制,允许由应用类加载器加载的 JDBC 驱动被核心类库调用。这种机制实际上打破了双亲委派模型,让应用类加载器加载的类可以被 Bootstrap 类加载器加载的类使用。
示例:
Class.forName("com.mysql.jdbc.Driver");
在这种情况下,com.mysql.jdbc.Driver
是由应用类加载器加载的,而 java.sql.DriverManager
是由 Bootstrap 类加载器加载的。通过线程上下文类加载器,DriverManager
可以正确加载驱动并使用它。
4. 热部署/热加载
背景:
热部署(Hot Deployment)是指应用程序在不停止 JVM 的情况下,更新、重载某些类或模块。这在开发 Web 应用或其他需要快速迭代的场景中非常有用。
为什么需要打破双亲委派?
在正常的双亲委派模型下,类加载器一旦加载了某个类,这个类就会一直驻留在内存中,除非 JVM 停止。如果需要在不关闭 JVM 的情况下更新某个类,必须打破双亲委派模型,使用自定义的类加载器来加载更新后的类。
热部署系统通常通过为每个版本的应用创建新的类加载器,并卸载旧的类加载器来实现类的重新加载。新的类加载器可以加载最新版本的类,而旧的类加载器及其加载的类最终会被垃圾回收,达到热加载的效果。
示例:
像 Spring Boot 或 Tomcat 等框架支持热部署,它们通过动态创建新的类加载器并将更新后的类加载到 JVM 中,从而实现应用的热更新。
- Bootstrap 类加载器
|
- Application ClassLoader v1 (老版本类)
|
- Application ClassLoader v2 (新版本类)
新的类加载器可以加载更新后的类,而旧的类加载器及其类将最终被回收。
5. 插件系统(如 Eclipse 插件)
背景:
插件系统允许应用程序通过加载外部插件来扩展功能。每个插件可能有自己独立的类库,甚至多个插件可能依赖同样名字的类但版本不同。
为什么需要打破双亲委派?
在插件系统中,插件通常是动态加载的,它们的类库可能与主程序或其他插件的类库冲突。如果严格按照双亲委派模型,插件的类库会被父类加载器加载,导致不同插件共享同一个类库,可能引发版本不一致等问题。
为了解决这个问题,插件系统通常为每个插件创建一个独立的类加载器,打破双亲委派模型,确保不同插件可以使用自己版本的类库。
示例:
Eclipse 的插件系统使用自定义类加载器为每个插件加载独立的类库。这种方式允许每个插件隔离运行,避免类库冲突。
- Bootstrap 类加载器
|
- Plugin ClassLoader 1 (插件 A 类)
|
- Plugin ClassLoader 2 (插件 B 类)
小结
打破双亲委派模型通常发生在需要类隔离、处理类库版本冲突或实现热部署的场景下。
常见的场景包括:
- 应用服务器/容器(如 Tomcat)中的类隔离。
- OSGi 模块化系统,不同模块加载不同版本的类。
- JDBC 驱动加载,通过线程上下文类加载器打破双亲委派。
- 热部署,通过自定义类加载器实现类的重载。
- 插件系统,为每个插件创建独立的类加载器,避免冲突。