前言
在探究 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)上所指定的类库。开发者可以直接使用这个类加载器,如果应用程序没有自定义类加载器,它通常是程序中的默认类加载器。
这些类加载器之间的关系一般如下图所示:
图中各个类加载器之间的关系被称为类加载器的双亲委派模型(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 类加载器体系有了初步了解,接下来将深入探讨其源码实现。由于篇幅所限,详细分析将在下一篇文章中展开。