当前位置: 首页 > news >正文

【JVM原理详解】08-类加载器实战-Tomcat类加载架构

类加载器实战:Tomcat类加载架构

前两篇我们学习了双亲委派模型及其被打破的场景(SPI/TCCL),这些更多是"框架层面"的机制。本篇我们将目光投向一个工业级的产品——Apache Tomcat。Tomcat是最流行的Java Web容器之一,它的类加载架构是打破双亲委派模型的经典案例,同时也是面试中的高频考点。理解Tomcat的类加载设计,不仅能帮你应对面试,更能在实际开发中排查Web应用类冲突问题。

Tomcat为什么需要自定义类加载架构

假设Tomcat使用标准的双亲委派模型,即所有Web应用共享同一个Application ClassLoader,会出现什么问题?

需求一:Web应用隔离

Tomcat作为Web容器,可以同时部署多个Web应用。这些应用可能依赖同一个第三方库的不同版本

应用A: 依赖Spring Framework 5.3.x 应用B: 依赖Spring Framework 6.0.x

如果共享同一个类加载器,这两个版本的Spring类会冲突——类加载器只会加载先出现的那一个版本。应用A可能用了6.0的Spring类导致运行异常,应用B反之亦然。

因此,每个Web应用必须有独立的类加载器,加载各自依赖的类库,实现相互隔离。

需求二:热部署

在开发过程中,修改了JSP文件后,希望浏览器刷新就能看到效果,不需要重启整个Tomcat。这意味着修改后的类需要被重新加载。但根据上一篇讲到的defineClass不可逆性,同一个类加载器无法重新加载已加载的类。因此,热部署必须创建新的类加载器实例,用新加载器重新加载修改后的类。

需求三:JVM核心类库的共享与安全

虽然Web应用之间需要隔离,但所有应用共享JVM核心类库(java.lang.*等)。这些类必须由Bootstrap ClassLoader统一加载,防止Web应用篡改。同时,Tomcat自身的类库也需要与Web应用的类库隔离——Tomcat的Servlet API实现不应被Web应用覆盖。

这三个需求决定了Tomcat不能简单地使用双亲委派模型,必须设计一套多层级的自定义类加载架构。

Tomcat类加载器层次结构

Tomcat的类加载器层次结构如下(以Tomcat 9/10为例):

┌─────────────────────────────┐ │ Bootstrap ClassLoader │ │ (加载JVM核心类库) │ └──────────────┬──────────────┘ │ ┌──────────────▼──────────────┐ │ System ClassLoader │ │ (= Application CL) │ │ (加载catalina.properties │ │ 中server.loader配置) │ └──────────────┬──────────────┘ │ ┌──────────────▼──────────────┐ │ Common ClassLoader │ │ (加载 $CATALINA_HOME/lib) │ │ Tomcat和Web应用共享的类 │ └──────┬───────────────┬───────┘ │ │ ┌────────────▼──┐ ┌──▼────────────┐ │ Catalina CL │ │ Shared CL │ │ (Tomcat内部 │ │ (所有WebApp │ │ 可见的类) │ │ 共享的类) │ └───────────────┘ └──┬─────────────┘ │ ┌────────────▼────────────┐ │ WebApp ClassLoader │ │ (每个Web应用独立) │ │ WEB-INF/classes + │ │ WEB-INF/lib │ └────────────┬────────────┘ │ ┌────────────▼────────────┐ │ JasperLoader │ │ (每个JSP文件独立) │ │ 热部署JSP编译后的类 │ └─────────────────────────┘

Common ClassLoader

Common ClassLoader加载$CATALINA_HOME/lib目录下的类库。这些类库对Tomcat内部和所有Web应用都可见。典型包含Servlet API的实现(如tomcat-coyote.jarservlet-api.jar等)。

Catalina ClassLoader

Catalina ClassLoader加载Tomcat服务器自身需要的类库,对Web应用不可见。通过catalina.properties中的server.loader配置指定路径。如果不配置server.loader,则Catalina ClassLoader等同于Common ClassLoader。

这样设计的目的是:Web应用看不到Tomcat内部的实现类,防止Web应用代码意外引用Tomcat内部类,提高隔离性。

Shared ClassLoader

Shared ClassLoader加载所有Web应用共享的类库,对Tomcat内部不可见。通过catalina.properties中的shared.loader配置指定路径。如果不配置shared.loader,则Shared ClassLoader等同于Common ClassLoader。

如果多个Web应用需要共享某个库(如统一的日志框架),可以将其放到shared.loader指定的路径下。

WebApp ClassLoader

WebApp ClassLoader是每个Web应用独立的类加载器,加载WEB-INF/classesWEB-INF/lib下的类。这是Tomcat类加载架构中最重要、也是打破双亲委派最彻底的类加载器。

JasperLoader

JasperLoader负责加载JSP文件编译后的Servlet类。每个JSP文件对应一个JasperLoader实例,当JSP文件被修改时,Tomcat会丢弃旧的JasperLoader,创建新的实例来加载重新编译后的类——这就是热部署的实现原理。

WebAppClassLoader如何打破双亲委派

WebAppClassLoader是Tomcat打破双亲委派的核心。标准双亲委派模型是"先委托父加载器,父加载器加载不了再自己加载",而WebAppClassLoader的加载策略是**“先自己加载,自己加载不了再委托父加载器”**。

WebAppClassLoader的加载顺序

Tomcat的WebappClassLoaderBase(WebAppClassLoader的基类)的加载顺序如下(简化版):

1. 检查本地缓存(该类是否已被本加载器加载过) 2. 检查JVM缓存(findLoadedClass,该类是否已被JVM加载过) 3. 用System ClassLoader(App CL)尝试加载 —— 这一步是为了防止Web应用覆盖J2SE核心类 —— 比如Web应用里有java.lang.String,不会加载它,先用System CL加载核心类 4. 使用本WebAppClassLoader自己尝试加载(findClass) —— 从WEB-INF/classes和WEB-INF/lib中查找 —— 这就是"优先自己加载",打破了双亲委派! 5. 如果自己加载不了,再委托给Common ClassLoader(父加载器)加载

为什么要先自己加载

先自己加载的原因是Web应用的类应该优先于容器提供的类。例如,Web应用可能依赖特定版本的Spring,而这个版本与Tomcat自带的库不同,应该用Web应用自己WEB-INF/lib下的版本。

为什么还要先用System ClassLoader加载J2SE类

第3步看似多余——既然要优先自己加载,为什么还要先让System ClassLoader加载?这是为了安全性:防止Web应用通过自定义java.lang.String等核心类来绕过安全限制。先让System ClassLoader加载J2SE类,就保证了核心类来自JVM而非Web应用。

关键源码解析

// Tomcat WebappClassLoaderBase.loadClass 源码(简化版,保留核心逻辑)@OverridepublicClass<?>loadClass(Stringname,booleanresolve)throwsClassNotFoundException{Class<?>clazz=null;// (1) 检查本地缓存clazz=findLoadedClass0(name);if(clazz!=null)returnclazz;// (2) 检查JVM缓存clazz=findLoadedClass(name);if(clazz!=null)returnclazz;// (3) 用System ClassLoader加载J2SE类(防止覆盖核心类)StringresourceName=binaryNameToPath(name,false);ClassLoaderjavaseLoader=getJavaseClassLoader();try{clazz=javaseLoader.loadClass(name);if(clazz!=null){if(resolve)resolveClass(clazz);returnclazz;}}catch(ClassNotFoundExceptione){// 忽略}// (4) 检查是否需要委托给父加载器(默认delegate=false,不委托)if(delegate){clazz=parent.loadClass(name);if(clazz!=null)returnclazz;}// (5) 自己尝试加载(WEB-INF/classes 和 WEB-INF/lib)try{clazz=findClass(name);if(clazz!=null)returnclazz;}catch(ClassNotFoundExceptione){// 忽略}// (6) 如果没委托过,最后再委托给父加载器if(!delegate){clazz=parent.loadClass(name);if(clazz!=null)returnclazz;}thrownewClassNotFoundException(name);}

通过配置控制委派策略

Tomcat允许通过WEB-INF/context.xml中的<Loader delegate="true/false"/>来控制委派策略:

  • delegate="false"(默认):先自己加载,再委托父加载器。推荐生产环境使用。
  • delegate="true":先委托父加载器,再自己加载。即标准双亲委派模式。适用于某些需要共享父加载器类库的场景。
<!-- WEB-INF/context.xml --><Context><Loaderdelegate="false"/></Context>

热部署的实现原理

热部署(Hot Deploy)是Tomcat类加载架构的重要应用场景,主要体现在JSP的热更新上。

JSP到Servlet的编译过程

当浏览器第一次请求一个JSP页面时,Tomcat的Jasper引擎会将JSP文件编译成一个Java Servlet类,然后编译成.class文件,最后用JasperLoader加载并实例化这个Servlet。

index.jsp ──(Jasper编译)──▶ index_jsp.java ──(javac)──▶ index_jsp.class │ ▼ JasperLoader加载 │ ▼ 实例化并执行

JSP热更新的检测机制

Tomcat默认开启JSP热更新检测(通过development参数控制):

<!-- WEB-INF/web.xml 中的JSP配置 --><jsp-config><jsp-property-group><url-pattern>*.jsp</url-pattern><el-ignored>false</el-ignored><scripting-invalid>false</scripting-invalid></jsp-property-group></jsp-config>

development=true模式下(默认),Jasper会定期检查(默认间隔3秒,由modificationTestInterval控制)JSP文件的最后修改时间。如果发现文件被修改,就触发重新编译和加载。

热更新的类加载器替换

当JSP被修改后,Tomcat的热更新流程如下:

1. 检测到 index.jsp 文件被修改 2. 丢弃旧的 JasperLoader 实例 ── 旧的 index_jsp.class 随之不可达,等待GC 3. 重新编译 index.jsp → 新的 index_jsp.java → 新的 index_jsp.class 4. 创建新的 JasperLoader 实例 5. 用新的 JasperLoader 加载新的 index_jsp.class 6. 实例化新的 Servlet,后续请求由新Servlet处理
// JasperLoader热更新的核心逻辑(简化版)publicclassJspRuntimeContext{privateMap<String,JspServletWrapper>jsps=newConcurrentHashMap<>();// 检查JSP是否需要重新编译publicvoidcheckCompile(){for(JspServletWrapperjsw:jsps.values()){if(jsw.isOutDated()){// JSP文件被修改了// 关键:创建新的JasperLoaderJasperLoadernewLoader=newJasperLoader(newURL[]{workDirUrl},// JSP编译输出目录jsw.getParentClassLoader());// 旧的JasperLoader被丢弃,旧类等待GCjsw.setClassLoader(newLoader);// 重新编译JSPjsw.compile();// 后续请求将使用新的JasperLoader加载新的Servlet类}}}}

热部署的内存泄漏风险

热部署有一个潜在问题:旧的JasperLoader和它加载的类不一定能被及时GC。如果旧Servlet的实例被其他地方引用(如Session中缓存了对象、静态变量持有引用等),那么旧JasperLoader就无法被回收,导致Metaspace/PermGen内存泄漏

这就是经典的"Tomcat热部署导致OutOfMemoryError: Metaspace(JDK 8前为PermGen space)"问题的根源。每次部署都会创建新的WebAppClassLoader/JasperLoader,如果旧的加载器无法回收,Metaspace持续增长直至溢出。

完整的类加载流程示例

假设在Tomcat中部署了一个Web应用,它依赖MySQL驱动并有一个JSP页面查询数据库:

请求: http://localhost:8080/myapp/query.jsp 1. Tomcat启动时: ├─ Common CL 加载 $CATALINA_HOME/lib/*.jar (包含servlet-api.jar) ├─ WebApp CL(myapp) 加载 WEB-INF/classes + WEB-INF/lib/*.jar (包含mysql.jar) └─ JasperLoader 加载编译后的 query_jsp.class 2. query.jsp执行时: ├─ 需要HttpServletResponse类 │ └─ WebApp CL先检查 → 没找到 → 委托Common CL → 找到servlet-api.jar中的类 ├─ 需要com.mysql.cj.jdbc.Driver类 │ └─ WebApp CL先自己查找 → 在WEB-INF/lib/mysql.jar中找到 → 加载成功 └─ 需要java.sql.DriverManager类 └─ WebApp CL先检查J2SE类 → System CL加载 → 找到rt.jar/java.base中的类 3. 如果修改了query.jsp: ├─ Jasper检测到文件修改 ├─ 丢弃旧JasperLoader ├─ 重新编译query.jsp → 新的query_jsp.class ├─ 创建新JasperLoader加载新的Servlet类 └─ 下次请求使用新Servlet

实践要点

  1. 避免在Web应用中打包Servlet API:有时开发者不小心将servlet-api.jar打包到WEB-INF/lib中,虽然WebAppClassLoader会先加载WEB-INF/lib下的版本,但由于Tomcat先让System/Common CL加载J2EE类(Servlet API在Common CL中),所以容器版本优先。但这可能导致版本不一致的诡异问题。建议通过<scope>provided</scope>排除:

    <dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>4.0.1</version><scope>provided</scope><!-- 由容器提供,不打包进war --></dependency>
  2. 共享库的正确放置位置

    • 只给Tomcat内部用:放server.loader路径
    • 所有Web应用共享:放shared.loader路径
    • 单个Web应用用:放WEB-INF/lib
    • Tomcat和所有Web应用共享:放$CATALINA_HOME/lib
  3. 热部署频率控制:频繁的热部署会导致大量JasperLoader创建和废弃。如果Metaspace空间不足,会触发频繁Full GC甚至OOM。生产环境通常关闭JSP热部署(development=false),开发环境适当设置modificationTestInterval减少检测频率。

  4. WebAppClassLoader与TCCL的配合:Tomcat在处理Web请求时,会将当前线程的上下文类加载器(TCCL)设置为对应Web应用的WebAppClassLoader。这确保了Web应用中通过SPI加载的服务(如JDBC驱动)能找到WEB-INF/lib下的实现类。

  5. Tomcat 10的命名空间变化:Tomcat 10+支持Jakarta EE,javax.servlet.*变为jakarta.servlet.*。这不是类加载器的变化,但迁移时需要注意类名空间冲突——同一个应用不能同时引用javaxjakarta的Servlet API。

小结

  • Tomcat自定义类加载架构的三大动因:Web应用隔离(各应用独立类空间)、热部署(重新加载修改后的类)、核心类库安全共享
  • Tomcat类加载器层次为:Common → Catalina/Shared → WebApp → JasperLoader,每一层负责不同范围的类加载。
  • WebAppClassLoader打破了双亲委派,采用"先自己加载、再委托父加载器"的策略,但会先用System CL加载J2SE核心类以保证安全。
  • 热部署通过创建新的JasperLoader实例重新加载JSP编译后的类实现,但需注意旧的类加载器回收问题,避免Metaspace内存泄漏。

下一篇,我们将把目光转向类加载问题的排查——当类冲突、类隔离引发NoClassDefFoundErrorLinkageError时,如何快速定位和解决。

http://www.cnnetsun.cn/news/3591578.html

相关文章:

  • 如何在3分钟内完成Windows系统极致优化:Talon一键精简工具完全指南
  • 市场专业的预制菜批发销售厂家哪家靠谱
  • AI写作工具在学术研究中的应用与优化策略
  • OpenMower终极指南:如何用700欧元打造智能RTK GPS割草机器人
  • OSG / osgEarth 入门知识体系
  • John the Ripper Jumbo:密码安全研究的终极武器与多架构并行计算框架
  • 终极指南:如何在macOS上完全解锁第三方鼠标侧键功能
  • 济南有哪些正规的民办中专
  • NodeBootstrap自动化测试指南:确保项目质量的完整流程
  • tesla_dashcam Docker部署指南:轻松实现GPU加速视频处理
  • 想玩 AI 恋人怕踩坑?看完这篇再下手
  • 团队协作中git的常用操作
  • Unity游戏IL2CPP逆向工程与安全防护实战
  • 震惊!选错试验机赔百万?苏州伺服拉力试验机必须注意这3点!
  • ArkUI 自定义组件与复用:构建高效 HarmonyOS 页面的核心技法
  • 2026年企业机票代订主流服务商盘点 服务能力与选型指南
  • Shodan_VirusTotal_CVE_CSDN
  • Peckham用户指南:解决import重复与路径搜索难题的实用技巧
  • 【DEIM 创新改进】CVPR 2026 | 独家创新首发、特征融合改进篇| 引入SAFusion语义对齐融合模块,助力无人机航拍、遥感影像、小目标检测、语义分割、实例分割、目标跟踪任务,有效涨点
  • 计算机毕业设计之智慧农业系统
  • 2026年7月上海企业搬迁公司推荐,深耕政企搬迁十多年,格睦搬家匠心护航企业办公乔迁安全
  • 同行不会告诉你的课题申报技巧
  • SpringBoot“尽所欲研”学习交流社区系统选题背景
  • 2026泰安黄金回收白银回收铂金回收工商备案可查全城上门回收旧金老店联系方式推荐
  • Windows下使用VirtualBox搭建ROS开发环境完整指南
  • wechatpay-apache-httpclient完全指南:10分钟上手微信支付APIv3开发
  • vt-py扩展开发:零基础构建自定义威胁分析工具的完整指南
  • 靠谱的奢石企业
  • 计算机Python毕设实战-基于 Python 的智能化无人零售超市管理信息系统 基于 Django 的无人超市运营管理系统设计【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • 如何使用cppclean快速定位C++项目中的未使用代码与冗余依赖