【Tomcat源码分析 】"深入探索:Tomcat 类加载机制揭秘"

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: 本文详细介绍了Java类加载机制及其在Tomcat中的应用。首先回顾了Java默认的类加载器,包括启动类加载器、扩展类加载器和应用程序类加载器,并解释了双亲委派模型的工作原理及其重要性。接着,文章分析了Tomcat为何不能使用默认类加载机制,因为它需要解决多个应用程序共存时的类库版本冲突、资源共享、类库隔离及JSP文件热更新等问题。最后,详细展示了Tomcat独特的类加载器设计,包括Common、Catalina、Shared、WebApp和Jsp类加载器,确保了系统的稳定性和安全性。通过这种设计,Tomcat实现了不同应用程序间的类库隔离与共享,同时支持JSP文件的热插拔。

前言

在探究 Tomcat 类加载机制之前,让我们重温一下 Java 默认的类加载器,加深对其的理解。 如同作者在《深入理解 Java 虚拟机》第二版中所言,类加载机制对于理解 Java 运行时环境至关重要。

什么是类加载机制

Java 虚拟机将描述类的字节码数据从 Class 文件加载至内存,并对其进行严格的校验、转换解析和初始化,最终生成可供虚拟机直接执行的 Java 类型。这一过程便是虚拟机的类加载机制。

虚拟机设计者将类加载阶段中“根据全限定名获取描述类信息的二进制字节流”这一关键步骤委托给了外部实现,赋予应用程序自行决定如何获取所需类的权利。负责执行这一任务的代码模块被称为“类加载器”。

类与类加载器的关系

类加载器虽然只负责加载类,但其影响却远超类加载阶段本身。对于任何一个类,它与加载它的类加载器共同决定了该类在 Java 虚拟机中的唯一性,就好比每个类加载器都拥有一个独立的“类仓库”,每个仓库中的类都是独一无二的。因此,判断两个类是否相同,只有在它们由同一个类加载器加载的前提下才有意义即使两个类来自同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,它们也必然被视为不同的类

什么是双亲委任模型

  1. 从 Java 虚拟机的视角来看,类加载器仅存在两种类型:一是启动类加载器(Bootstrap ClassLoader),它由 C++语言实现(仅限于 HotSpot 虚拟机),是虚拟机自身的一部分;二是所有其他类加载器,它们均由 Java 语言实现,独立于虚拟机外部,并且都继承自抽象类 java.lang.ClassLoader。
  2. 从 Java 开发者的角度,类加载器可以更细致地划分,大部分 Java 程序员会接触到以下三种系统提供的类加载器:
    1. 启动类加载器(Bootstrap ClassLoader):它负责加载位于 JAVA_HOME/lib 目录下的,或者被-Xbootclasspath 参数指定的路径中的,并且被虚拟机识别的类库(仅根据文件名识别,例如 rt.jar,其他名字的类库即使放在 lib 目录下也不会被重载)。
    2. 扩展类加载器(Extension ClassLoader):由 sun.misc.Launcher\$ExtClassLoader 实现,它负责加载位于 JAVA_HOME/lib/ext 目录下的,或由 java.ext.dirs 系统变量指定的路径中的所有类库。开发者可以直接使用扩展类加载器。
    3. 应用程序类加载器(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 容器,其存在的意义何在? 到底是为了解决怎样的问题?

  1. Web 容器或需承载多个应用程序,而不同应用可能依赖于同一第三方类库的不同版本。为确保应用间相互隔离,每个应用程序的类库应保持独立,避免彼此干扰。
  2. 同一 Web 容器中的相同类库版本可共享,以避免资源浪费。若每个应用程序都独立加载相同类库,则当服务器承载十个应用程序时,将会加载十份相同的类库,这无疑是极不合理的。
  3. Web 容器自身亦有其依赖的类库,不可与应用程序的类库混淆。出于安全考虑,容器的类库与应用程序的类库应严格隔离,互不干扰。
  4. 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 类加载器体系有了初步了解,接下来将深入探讨其源码实现。由于篇幅所限,详细分析将在下一篇文章中展开。

如有问题,欢迎微信搜索【码上遇见你】。

好了,本章节到此告一段落。希望对你有所帮助,祝学习顺利。

相关文章
|
12天前
|
监控 网络协议 应用服务中间件
【Tomcat源码分析】从零开始理解 HTTP 请求处理 (第一篇)
本文详细解析了Tomcat架构中复杂的`Connector`组件。作为客户端与服务器间沟通的桥梁,`Connector`负责接收请求、封装为`Request`和`Response`对象,并传递给`Container`处理。文章通过四个关键问题逐步剖析了`Connector`的工作原理,并深入探讨了其构造方法、`init()`与`start()`方法。通过分析`ProtocolHandler`、`Endpoint`等核心组件,揭示了`Connector`初始化及启动的全过程。本文适合希望深入了解Tomcat内部机制的读者。欢迎关注并点赞,持续更新中。如有问题,可搜索【码上遇见你】交流。
【Tomcat源码分析】从零开始理解 HTTP 请求处理 (第一篇)
|
14天前
|
人工智能 前端开发 Java
【Tomcat源码分析】启动过程深度解析 (二)
本文深入探讨了Tomcat启动Web应用的过程,重点解析了其加载ServletContextListener及Servlet的机制。文章从Bootstrap反射调用Catalina的start方法开始,逐步介绍了StandardServer、StandardService、StandardEngine、StandardHost、StandardContext和StandardWrapper的启动流程。每个组件通过Lifecycle接口协调启动,子容器逐层启动,直至整个服务器完全启动。此外,还详细分析了Pipeline及其Valve组件的作用,展示了Tomcat内部组件间的协作机制。
【Tomcat源码分析】启动过程深度解析 (二)
|
28天前
|
设计模式 应用服务中间件 容器
【Tomcat源码分析】Pipeline 与 Valve 的秘密花园
本文深入剖析了Tomcat中的Pipeline和Valve组件。Valve作为请求处理链中的核心组件,通过接口定义了关键方法;ValveBase为其基类,提供了通用实现。Pipeline则作为Valve容器,通过首尾相连的Valve链完成业务处理。StandardPipeline实现了Pipeline接口,提供了详细的Valve管理逻辑。通过对代码的详细分析,揭示了模板方法模式和责任链模式的应用,展示了系统的扩展性和模块化设计。
【Tomcat源码分析】Pipeline 与 Valve 的秘密花园
|
1月前
|
设计模式 人工智能 安全
【Tomcat源码分析】生命周期机制 Lifecycle
Tomcat内部通过各种组件协同工作,构建了一个复杂的Web服务器架构。其中,`Lifecycle`机制作为核心,管理组件从创建到销毁的整个生命周期。本文详细解析了Lifecycle的工作原理及其方法,如初始化、启动、停止和销毁等关键步骤,并展示了LifecycleBase类如何通过状态机和模板模式实现这一过程。通过深入理解Lifecycle,我们可以更好地掌握组件生命周期管理,提升系统设计能力。欢迎关注【码上遇见你】获取更多信息,或搜索【AI贝塔】体验免费的Chat GPT。希望本章内容对你有所帮助。
|
2月前
|
网络协议 Java 应用服务中间件
Tomcat源码分析 (一)----- 手撕Java Web服务器需要准备哪些工作
本文探讨了后端开发中Web服务器的重要性,特别是Tomcat框架的地位与作用。通过解析Tomcat的内部机制,文章引导读者理解其复杂性,并提出了一种实践方式——手工构建简易Web服务器,以此加深对Web服务器运作原理的认识。文章还详细介绍了HTTP协议的工作流程,包括请求与响应的具体格式,并通过Socket编程在Java中的应用实例,展示了客户端与服务器间的数据交换过程。最后,通过一个简单的Java Web服务器实现案例,说明了如何处理HTTP请求及响应,强调虽然构建基本的Web服务器相对直接,但诸如Tomcat这样的成熟框架提供了更为丰富和必要的功能。
|
3月前
|
算法 Java 应用服务中间件
开发与运维机制问题之在Tomcat的类加载机制中,如果BootstrapClassLoader没有加载成功类,Tomca如何解决
开发与运维机制问题之在Tomcat的类加载机制中,如果BootstrapClassLoader没有加载成功类,Tomca如何解决
18 0
|
5月前
|
前端开发 Java 应用服务中间件
|
5月前
|
缓存 前端开发 安全
细究Java类加载机制和Tomcat类加载机制
细究Java类加载机制和Tomcat类加载机制
45 0
|
5月前
|
XML Java 应用服务中间件
SpringBoot配置外部Tomcat项目启动流程源码分析(长文)
SpringBoot配置外部Tomcat项目启动流程源码分析(长文)
394 0