【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 ClassLoaderCommon ClassLoader加载$CATALINA_HOME/lib目录下的类库。这些类库对Tomcat内部和所有Web应用都可见。典型包含Servlet API的实现如tomcat-coyote.jar、servlet-api.jar等。Catalina ClassLoaderCatalina ClassLoader加载Tomcat服务器自身需要的类库对Web应用不可见。通过catalina.properties中的server.loader配置指定路径。如果不配置server.loader则Catalina ClassLoader等同于Common ClassLoader。这样设计的目的是Web应用看不到Tomcat内部的实现类防止Web应用代码意外引用Tomcat内部类提高隔离性。Shared ClassLoaderShared ClassLoader加载所有Web应用共享的类库对Tomcat内部不可见。通过catalina.properties中的shared.loader配置指定路径。如果不配置shared.loader则Shared ClassLoader等同于Common ClassLoader。如果多个Web应用需要共享某个库如统一的日志框架可以将其放到shared.loader指定的路径下。WebApp ClassLoaderWebApp ClassLoader是每个Web应用独立的类加载器加载WEB-INF/classes和WEB-INF/lib下的类。这是Tomcat类加载架构中最重要、也是打破双亲委派最彻底的类加载器。JasperLoaderJasperLoader负责加载JSP文件编译后的Servlet类。每个JSP文件对应一个JasperLoader实例当JSP文件被修改时Tomcat会丢弃旧的JasperLoader创建新的实例来加载重新编译后的类——这就是热部署的实现原理。WebAppClassLoader如何打破双亲委派WebAppClassLoader是Tomcat打破双亲委派的核心。标准双亲委派模型是先委托父加载器父加载器加载不了再自己加载而WebAppClassLoader的加载策略是**“先自己加载自己加载不了再委托父加载器”**。WebAppClassLoader的加载顺序Tomcat的WebappClassLoaderBaseWebAppClassLoader的基类的加载顺序如下简化版1. 检查本地缓存该类是否已被本加载器加载过 2. 检查JVM缓存findLoadedClass该类是否已被JVM加载过 3. 用System ClassLoaderApp 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?clazznull;// (1) 检查本地缓存clazzfindLoadedClass0(name);if(clazz!null)returnclazz;// (2) 检查JVM缓存clazzfindLoadedClass(name);if(clazz!null)returnclazz;// (3) 用System ClassLoader加载J2SE类防止覆盖核心类StringresourceNamebinaryNameToPath(name,false);ClassLoaderjavaseLoadergetJavaseClassLoader();try{clazzjavaseLoader.loadClass(name);if(clazz!null){if(resolve)resolveClass(clazz);returnclazz;}}catch(ClassNotFoundExceptione){// 忽略}// (4) 检查是否需要委托给父加载器默认delegatefalse不委托if(delegate){clazzparent.loadClass(name);if(clazz!null)returnclazz;}// (5) 自己尝试加载WEB-INF/classes 和 WEB-INF/libtry{clazzfindClass(name);if(clazz!null)returnclazz;}catch(ClassNotFoundExceptione){// 忽略}// (6) 如果没委托过最后再委托给父加载器if(!delegate){clazzparent.loadClass(name);if(clazz!null)returnclazz;}thrownewClassNotFoundException(name);}通过配置控制委派策略Tomcat允许通过WEB-INF/context.xml中的Loader delegatetrue/false/来控制委派策略delegatefalse默认先自己加载再委托父加载器。推荐生产环境使用。delegatetrue先委托父加载器再自己加载。即标准双亲委派模式。适用于某些需要共享父加载器类库的场景。!-- WEB-INF/context.xml --ContextLoaderdelegatefalse//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-configjsp-property-groupurl-pattern*.jsp/url-patternel-ignoredfalse/el-ignoredscripting-invalidfalse/scripting-invalid/jsp-property-group/jsp-config在developmenttrue模式下默认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{privateMapString,JspServletWrapperjspsnewConcurrentHashMap();// 检查JSP是否需要重新编译publicvoidcheckCompile(){for(JspServletWrapperjsw:jsps.values()){if(jsw.isOutDated()){// JSP文件被修改了// 关键创建新的JasperLoaderJasperLoadernewLoadernewJasperLoader(newURL[]{workDirUrl},// JSP编译输出目录jsw.getParentClassLoader());// 旧的JasperLoader被丢弃旧类等待GCjsw.setClassLoader(newLoader);// 重新编译JSPjsw.compile();// 后续请求将使用新的JasperLoader加载新的Servlet类}}}}热部署的内存泄漏风险热部署有一个潜在问题旧的JasperLoader和它加载的类不一定能被及时GC。如果旧Servlet的实例被其他地方引用如Session中缓存了对象、静态变量持有引用等那么旧JasperLoader就无法被回收导致Metaspace/PermGen内存泄漏。这就是经典的Tomcat热部署导致OutOfMemoryError: MetaspaceJDK 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实践要点避免在Web应用中打包Servlet API有时开发者不小心将servlet-api.jar打包到WEB-INF/lib中虽然WebAppClassLoader会先加载WEB-INF/lib下的版本但由于Tomcat先让System/Common CL加载J2EE类Servlet API在Common CL中所以容器版本优先。但这可能导致版本不一致的诡异问题。建议通过scopeprovided/scope排除dependencygroupIdjavax.servlet/groupIdartifactIdjavax.servlet-api/artifactIdversion4.0.1/versionscopeprovided/scope!-- 由容器提供不打包进war --/dependency共享库的正确放置位置只给Tomcat内部用放server.loader路径所有Web应用共享放shared.loader路径单个Web应用用放WEB-INF/libTomcat和所有Web应用共享放$CATALINA_HOME/lib热部署频率控制频繁的热部署会导致大量JasperLoader创建和废弃。如果Metaspace空间不足会触发频繁Full GC甚至OOM。生产环境通常关闭JSP热部署developmentfalse开发环境适当设置modificationTestInterval减少检测频率。WebAppClassLoader与TCCL的配合Tomcat在处理Web请求时会将当前线程的上下文类加载器TCCL设置为对应Web应用的WebAppClassLoader。这确保了Web应用中通过SPI加载的服务如JDBC驱动能找到WEB-INF/lib下的实现类。Tomcat 10的命名空间变化Tomcat 10支持Jakarta EEjavax.servlet.*变为jakarta.servlet.*。这不是类加载器的变化但迁移时需要注意类名空间冲突——同一个应用不能同时引用javax和jakarta的Servlet API。小结Tomcat自定义类加载架构的三大动因Web应用隔离各应用独立类空间、热部署重新加载修改后的类、核心类库安全共享。Tomcat类加载器层次为Common → Catalina/Shared → WebApp → JasperLoader每一层负责不同范围的类加载。WebAppClassLoader打破了双亲委派采用先自己加载、再委托父加载器的策略但会先用System CL加载J2SE核心类以保证安全。热部署通过创建新的JasperLoader实例重新加载JSP编译后的类实现但需注意旧的类加载器回收问题避免Metaspace内存泄漏。下一篇我们将把目光转向类加载问题的排查——当类冲突、类隔离引发NoClassDefFoundError、LinkageError时如何快速定位和解决。

相关新闻

前后端分离架构

前后端分离架构

GoViewBoard 系统中前端和后端的联系: 🏗️ 架构概述 这个系统采用 前后端分离架构,包含两个独立的部分: 后端 (Backend) 技术栈: Go Gin 框架端口: 8080功能: 提供 RESTful API 接口,处理业务逻辑,数据库…

2026/7/22 22:34:01阅读更多 →
如何将iPad备份到外置硬盘(4种经过验证的方法)

如何将iPad备份到外置硬盘(4种经过验证的方法)

现代 iPad,尤其是搭载 M2/M4 芯片的 iPad Pro 和 iPad Air 机型,通常会存储数百 GB 的 4K ProRes 视频、Procreate 绘图、RAW 照片和系统文件。然而,当您在Mac或Windows PC 上备份 iPad时,苹果的原生软件(Finder、iTun…

2026/7/22 22:34:01阅读更多 →
Windows 下 Nginx + Flask 应用迁移阿里云完整部署指南(附安全加固)

Windows 下 Nginx + Flask 应用迁移阿里云完整部署指南(附安全加固)

阿里云 Nginx Flask 应用部署配置文档 适用场景:将现有 Flask 应用(监听 5000 端口)迁移到阿里云服务器,使用 Nginx 提供 HTTPS 反向代理,并确保后端服务仅限本机访问。 一、环境准备 1. 服务器信息(示例…

2026/7/22 22:34:01阅读更多 →
AndroidNavigation性能调优:避免常见陷阱的7个关键点

AndroidNavigation性能调优:避免常见陷阱的7个关键点

AndroidNavigation性能调优:避免常见陷阱的7个关键点 【免费下载链接】AndroidNavigation A library managing navigation, nested Fragment, StatusBar, Toolbar for Android 项目地址: https://gitcode.com/gh_mirrors/an/AndroidNavigation AndroidNaviga…

2026/7/22 23:28:24阅读更多 →
AndroidNavigation在大型项目中的应用:架构设计与模块化实践

AndroidNavigation在大型项目中的应用:架构设计与模块化实践

AndroidNavigation在大型项目中的应用:架构设计与模块化实践 【免费下载链接】AndroidNavigation A library managing navigation, nested Fragment, StatusBar, Toolbar for Android 项目地址: https://gitcode.com/gh_mirrors/an/AndroidNavigation Androi…

2026/7/22 23:28:24阅读更多 →
树莓派部署BirdNET-Go常见问题解答:从硬件要求到性能优化

树莓派部署BirdNET-Go常见问题解答:从硬件要求到性能优化

树莓派部署BirdNET-Go常见问题解答:从硬件要求到性能优化 【免费下载链接】birdnet-go Self-hosted realtime soundscape analyser for birds, bats and other wildlife. Multi-model local AI inference, runs 24/7 on a Raspberry Pi. 项目地址: https://gitcod…

2026/7/22 23:28:24阅读更多 →
gh_mirrors/log/logback授权控制详解:基于角色与权限的访问管理

gh_mirrors/log/logback授权控制详解:基于角色与权限的访问管理

gh_mirrors/log/logback授权控制详解:基于角色与权限的访问管理 【免费下载链接】logback 💡 SpringBootSpring Security基本配置 项目地址: https://gitcode.com/gh_mirrors/log/logback gh_mirrors/log/logback是一个基于SpringBootSpring Secu…

2026/7/22 23:28:24阅读更多 →
终极指南:OpenSpeedy如何通过Ring3 Hook技术实现游戏时间函数拦截与帧率控制优化

终极指南:OpenSpeedy如何通过Ring3 Hook技术实现游戏时间函数拦截与帧率控制优化

终极指南:OpenSpeedy如何通过Ring3 Hook技术实现游戏时间函数拦截与帧率控制优化 【免费下载链接】OpenSpeedy 🎮 An open-source game speed modifier. 项目地址: https://gitcode.com/gh_mirrors/op/OpenSpeedy OpenSpeedy是一款基于Ring3级别H…

2026/7/22 23:28:24阅读更多 →
做公司PPT最烦的不是写内容,是套模板

做公司PPT最烦的不是写内容,是套模板

近两年来,利用AI生成PPT这样的事情普及开来的速度还是比较快的。输入那么一小段需求,几十秒钟就能够弄出那么一份结构比较完整的PPT,内容方面的逻辑还算可以,排版也勉强说得过去 。 但问题出在拿到之后,许多公司皆拥有…

2026/7/22 23:26:24阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/22 0:53:59阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/22 0:53:59阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/22 0:53:59阅读更多 →
中小企业小程序开发公司怎么选:预算、上手和售后避坑指南

中小企业小程序开发公司怎么选:预算、上手和售后避坑指南

中小企业做小程序,最常见的矛盾是预算有限,但又不希望功能太单薄;没有技术团队,但又希望后续能自己运营;想快速上线,又担心隐性收费和售后失联。选型时如果只看“低价套餐”或“案例数量”,很容…

2026/7/22 0:01:17阅读更多 →
GEO优化如何沉淀长期内容资产?广拓时代谈AI搜索时代的内容ROI

GEO优化如何沉淀长期内容资产?广拓时代谈AI搜索时代的内容ROI

企业做营销,最怕钱花完了,资产没有留下。 效果广告能带来一段时间的曝光,但预算停止后,流量往往也随之停止。短视频内容可能在几天内冲高,也可能很快沉下去。AI搜索时代,企业需要重新思考一个问题&#xff…

2026/7/22 0:01:17阅读更多 →
Agent 终态判定:何时该停止思考、给出最终回复

Agent 终态判定:何时该停止思考、给出最终回复

Agent 终态判定:何时该停止思考、给出最终回复 一、你的 Agent 在"再想想"的循环里绕了 12 轮,用户已经关窗口了 Agent 与人最大的区别是:人知道什么时候该停下来给答案,Agent 会一直"想"下去。你给 Agent 接…

2026/7/22 0:01:17阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/22 22:56:18阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/22 18:55:50阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/22 18:55:50阅读更多 →