为什么 Tomcat 要打破 JVM 的双亲委派模型?
Tomcat 确实打破了 JVM 标准的“双亲委派模型”(Parent Delegation Model),但这主要发生在 Web 应用程序(WebApp) 的类加载层面。
简单来说,Tomcat 这样做是为了解决 Web 容器特有的 隔离性(Isolation) 和 灵活性 问题。
以下是 Tomcat 打破双亲委派模型的四个核心原因:
1. 依赖版本隔离(最主要原因)
场景: 假设你在 Tomcat 中部署了两个 Web 应用:
- 应用 A 使用了 Spring 4.0。
- 应用 B 使用了 Spring 5.0。
如果遵循双亲委派:
类加载器会委托给父加载器加载 Spring jar 包。由于 JVM 中全限定名(包名+类名)相同的类只能被加载一次,那么先加载了 Spring 4,应用 B 就会报错;反之应用 A 会报错。
Tomcat 的做法:
每个 Web 应用都有自己独立的 WebAppClassLoader。当加载类时,它优先在自己的 WEB-INF/lib 和 WEB-INF/classes 下寻找,找不到才委托给父加载器。这样,应用 A 和应用 B 就可以各自拥有独立的 Spring 版本,互不干扰。
2. 服务器自身与应用的隔离
场景: Tomcat 作为一个 Java 程序,自身也依赖许多第三方库(如 commons-logging, servlet-api 等)。Web 应用可能也依赖了这些库,但版本不同。
如果遵循双亲委派:
如果 Web 应用的类加载委托给系统加载器(AppClassLoader),而 Tomcat 的类库也在系统路径下,那么 Web 应用可能会意外使用到 Tomcat 内部的类库版本,导致兼容性问题。或者,Web 应用可能会覆盖 Tomcat 自身的逻辑,导致服务器不稳定。
Tomcat 的做法:
Tomcat 使用 CatalinaClassLoader 加载服务器内部类,使用 WebAppClassLoader 加载应用类。通过打破双亲委派(优先加载应用内的),保证了 Web 应用可以使用自己捆绑的库,而不会被 Tomcat 服务器的库覆盖(或者反过来覆盖服务器的库)。
3. JSP 的热部署(Hot Swap)
场景: 在开发时,我们修改了 JSP 文件,希望不需要重启服务器就能看到效果。
原理: JSP 文件最终会被编译成 .class 文件(Servlet)。在 JVM 中,一个类一旦被加载,就无法被“卸载”或“替换”,除非卸载加载它的 ClassLoader。
Tomcat 的做法:
Tomcat 为每个 JSP 文件都分配了一个独立的类加载器(JasperLoader)。当检测到 JSP 文件被修改时,Tomcat 会直接丢弃旧的类加载器,创建一个新的类加载器来加载修改后的 JSP 类。这种机制天然就不适合双亲委派(因为双亲委派强调只要父加载器能加载,子加载器就别动,这会导致无法加载新的类版本)。
4. 共享公共库
虽然 Tomcat 打破了隔离性,但它也保留了共享机制。
场景: 如果有 10 个应用都使用标准的 Servlet API,没必要在内存里加载 10 份。
Tomcat 的做法:
Tomcat 引入了 CommonClassLoader。对于放在 tomcat/lib 下的 jar 包,遵循双亲委派机制。Web 应用优先找自己的,找不到再找 Common 的。这实现了“既能隔离(应用私有),又能共享(通用库)”。
总结:Tomcat 的类加载逻辑
为了实现上述目标,Tomcat 的 WebAppClassLoader 的加载逻辑(loadClass 方法)大致如下,这正是“打破”双亲委派的体现:
- 检查缓存: 看该类是否已经加载过。
- JVM 核心类检查(关键): 如果是
java.*开头的类(如java.lang.String),必须委托给 JVM 的 Bootstrap ClassLoader 加载。这是为了安全性,防止恶意代码覆盖核心库。 - 加载应用自身类(打破点): 尝试从
WEB-INF/classes和WEB-INF/lib加载。(注意:标准模型是先委托父类,这里是先自己加载) - 委托父类: 如果自己没找到,才委托给父类加载器(CommonClassLoader)去加载。
一句话总结: Tomcat 打破双亲委派是为了实现 “Web 应用之间的类库版本隔离” 以及 “Web 应用与服务器运行时的隔离”,同时保证核心 Java 类库的安全。