前言
在探究 Tomcat 类加载机制之前,让我们重温一下 Java 默认的类加载器,加深对其的理解。 如同作者在《深入理解 Java 虚拟机》第二版中所言,类加载机制对于理解 Java 运行时环境至关重要。
什么是类加载机制
Java 虚拟机将描述类的字节码数据从 Class 文件加载至内存,并对其进行严格的校验、转换解析和初始化,最终生成可供虚拟机直接执行的 Java 类型。这一过程便是虚拟机的类加载机制。
虚拟机设计者将类加载阶段中“根据全限定名获取描述类信息的二进制字节流”这一关键步骤委托给了外部实现,赋予应用程序自行决定如何获取所需类的权利。负责执行这一任务的代码模块被称为“类加载器”。
类与类加载器的关系
类加载器虽然只负责加载类,但其影响却远超类加载阶段本身。对于任何一个类,它与加载它的类加载器共同决定了该类在 Java 虚拟机中的唯一性,就好比每个类加载器都拥有一个独立的“类仓库”,每个仓库中的类都是独一无二的。因此,判断两个类是否相同,只有在它们由同一个类加载器加载的前提下才有意义。即使两个类来自同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,它们也必然被视为不同的类。
什么是双亲委任模型
- 从 Java 虚拟机的视角来看,类加载器仅存在两种类型:一是
启动类加载器
(Bootstrap ClassLoader),它由 C++语言实现(仅限于 HotSpot 虚拟机),是虚拟机自身的一部分;二是所有其他类加载器
,它们均由 Java 语言实现,独立于虚拟机外部,并且都继承自抽象类 java.lang.ClassLoader。 - 从 Java 开发者的角度,类加载器可以更细致地划分,大部分 Java 程序员会接触到以下三种系统提供的类加载器:
- 启动类加载器(Bootstrap ClassLoader):它负责加载位于 JAVA_HOME/lib 目录下的,或者被-Xbootclasspath 参数指定的路径中的,并且被虚拟机识别的类库(仅根据文件名识别,例如 rt.jar,其他名字的类库即使放在 lib 目录下也不会被重载)。
- 扩展类加载器(Extension ClassLoader):由 sun.misc.Launcher\$ExtClassLoader 实现,它负责加载位于 JAVA_HOME/lib/ext 目录下的,或由 java.ext.dirs 系统变量指定的路径中的所有类库。开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):由 sun.misc.Launcher\$AppClassLoader 实现,由于它是 ClassLoader 中的 getSystemClassLoader 方法的返回值,因此也被称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用程序没有自定义类加载器,它通常是程序中的默认类加载器。
3.
这些类加载器之间的关系一般如下图所示:
图中各个类加载器之间的关系被称为类加载器的双亲委派模型(Parents Delegation Mode)。双亲委派模型规定,除了顶层的启动类加载器之外,其他所有类加载器都应该由其父类加载器加载。这里类加载器之间的父子关系通常不通过继承实现,而是使用组合关系来复用父加载器的代码。
类加载器的双亲委派模型在 JDK 1.2 时期被引入,并被广泛应用于之后的 Java 程序中,但它并非强制性约束模型,而是 Java 设计者推荐给开发者的一种类加载器实现方式。
双亲委派模型的工作流程如下:当一个类加载器收到类加载请求时,它不会立即尝试加载该类,而是将请求委托给父类加载器处理。每一层级类加载器都遵循这一原则,最终请求将传递到顶层的启动类加载器。只有当父加载器反馈无法完成请求(在其搜索范围内没有找到所需的类)时,子加载器才会尝试自己加载。
为什么要使用双亲委派模型
如果没有使用双亲委派模型,而是由各个类加载器自行加载类,那么如果用户编写了一个名为java.lang.Object
的类并将其放置在程序的 ClassPath 中,系统中就会出现多个不同的 Object 类。Java 类型体系中最基础的行为将无法保证,应用程序也将变得混乱不堪。
双亲委任模型时如何实现的
非常简单,双亲委派模型的核心逻辑体现在 java.lang.ClassLoader 中的 loadClass 方法中。
首先判断若类尚未加载,则委派父加载器尝试加载。父加载器为空时,则默认委托启动类加载器。若父加载器加载失败,则抛出 ClassNotFoundException 异常,随后调用自定义 findClass 方法进行加载。
如何破坏双亲委任模型
双亲委派模型并非强制性约束,而是 Java 设计者推荐的类加载器实现方式。虽然大部分类加载器都遵循这一模型,但也有例外。迄今为止,双亲委派模型曾三次被“打破”。
第一次发生在双亲委派模型出现之前,即 JDK 1.2 发布之前。
第二次则是模型本身的缺陷所致。双亲委派模型有效地解决了基础类的统一加载问题(越基础的类由越上层的加载器加载),然而,并非所有基础类都只被用户代码调用。如果基础类需要调用用户代码,就会出现问题。
这并非不可能。JNDI 服务就是一个典型例子。作为 Java 的标准服务,JNDI 的代码由启动类加载器加载(在 JDK 1.3 时就已包含在 rt.jar 中),但它需要调用独立厂商实现并部署在应用程序 ClassPath 下的 JNDI 接口提供者(SPI,Service Provider Interface)的代码。然而,启动类加载器无法“识别”这些代码,因为它们并不在 rt.jar 中。为了解决这个问题,启动类加载器需要加载这些代码。
为了解决这个问题,Java 设计团队引入了一个名为线程上下文类加载器(Thread Context ClassLoader)的设计。这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader 方法进行设置。如果在创建线程时尚未设置,它会从父线程中继承一个;如果在应用程序的全局范围内都没有设置,那么这个类加载器默认就是应用程序类加载器。
有了线程上下文加载器,JNDI 服务便可以使用它来加载所需的 SPI 代码。这相当于父类加载器请求子类加载器完成类加载,打破了双亲委派模型的层次结构,逆向使用类加载器,实际上已经违背了模型的一般性原则。但这是无奈之举,Java 中所有涉及 SPI 加载的动作基本上都采用这种方式,例如 JNDI、JDBC、JCE、JAXB、JBI 等。
第三次破坏则是为了实现热插拔、热部署、模块化。这意味着添加或删除功能无需重启,只需将模块连同其类加载器一起替换,即可实现代码热替换。
Tomcat 的类加载器是怎么设计的
首先,我们来思考个问题:
Tomcat 如果使用默认的类加载机制行不行?
细细想一下,Tomcat 作为一款 Web 容器,其存在的意义何在? 到底是为了解决怎样的问题?
- Web 容器或需承载多个应用程序,而不同应用可能依赖于同一第三方类库的不同版本。为确保应用间相互隔离,每个应用程序的类库应保持独立,避免彼此干扰。
- 同一 Web 容器中的相同类库版本可共享,以避免资源浪费。若每个应用程序都独立加载相同类库,则当服务器承载十个应用程序时,将会加载十份相同的类库,这无疑是极不合理的。
- Web 容器自身亦有其依赖的类库,不可与应用程序的类库混淆。出于安全考虑,容器的类库与应用程序的类库应严格隔离,互不干扰。
- Web 容器需具备对 JSP 文件修改的支持。众所周知,JSP 文件最终需编译成 Class 文件才能在虚拟机中运行。然而,程序运行后修改 JSP 文件已成常态,否则容器便无实际意义。因此,Web 容器应支持 JSP 修改后无需重启服务器,以提高开发效率。
再回头看问题,Tomcat 如果使用默认的类加载机制行不行?
答案是不行的。为什么?
首先,默认的类加载器机制无法加载相同类库的不同版本。其机制只关注全限定类名,而不会区分版本。因此,第一个和第三个问题无法通过默认机制解决。
其次,默认类加载器的职责正是确保类库的唯一性,这与第二个问题并不冲突。
至于第四个问题,热修改 JSP 文件面临挑战。 JSP 文件最终编译成 Class 文件,修改后的 JSP 文件仍拥有相同的类名,导致类加载器直接从方法区中获取已存在的 Class 文件,无法加载修改后的内容。
为了解决这个问题,可以为每个 JSP 文件创建唯一的类加载器。当 JSP 文件修改后,直接卸载该类加载器,并重新创建类加载器,从而重新加载修改后的 JSP 文件。
Tomcat 如何实现自己独特的类加载机制
首先看下 Tomcat 的设计图:
观察这张图,我们看到了多个类加载器,其中除了 JDK 自带的类加载器之外,我们尤其关注 Tomcat 自身持有的类加载器。细细观察,我们会发现 Catalina 类加载器和 Shared 类加载器并非父子关系,而是兄弟关系。这种设计背后的缘由,需要我们分析每个类加载器的用途才能明了。
从图中我们能了解到 Tomcat 类加载器体系结构的设计精妙,每个类加载器各司其职,确保了系统的稳定性和安全性。
- Common 类加载器 负责加载 Tomcat 和 Web 应用共同复用的类,例如日志框架、通用工具库等。
- Catalina 类加载器 专注于加载 Tomcat 自身的类,这些类在 Web 应用中不可见,确保了 Tomcat 核心功能的独立性。
- Shared 类加载器 负责加载所有 Web 应用共同复用的类,例如数据库连接池、缓存框架等,这些类在 Tomcat 中不可见,避免了应用之间的冲突。
- WebApp 类加载器 为每个 Web 应用单独创建,负责加载该应用的类,这些类在 Tomcat 和其他应用中不可见,确保了应用之间的隔离。
- Jsp 类加载器 为每个 JSP 页面创建唯一的类加载器,方便实现 JSP 页面的热插拔,提高开发效率。
至此,我们对 Tomcat 类加载器体系有了初步了解,接下来将深入探讨其源码实现。由于篇幅所限,详细分析将在下一篇文章中展开。
如有问题,欢迎微信搜索【码上遇见你】。
好了,本章节到此告一段落。希望对你有所帮助,祝学习顺利。