tomcat(四)源码分析——tomcat类加载器

本文深入探讨了Tomcat的类加载机制,包括Java类加载器的基本概念、双亲委派模型及其在Tomcat中的扩展,以及Tomcat如何解决类库隔离性、灵活性和性能等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文链接:https://blog.youkuaiyun.com/w1992wishes/article/details/79274417

一前言

下载tomcat解压后,可以在webapps目录下看到几个文件夹(这些都是web应用),webapps对应到tomcat容器中的Host,里面的文件夹则对应到Context。tomcat启动后,webapps下的所有web应用都可以提供服务。

那么就有一个问题,假如webapps下有两个应用app1和app2,它们有各自独立依赖的jar包,又有共同依赖的jar包,这些相同的jar包有些版本相同,有些又不相同,这种情况下,tomcat是如何加载这些jar包的呢?

带着这个疑问,一步步来分析tomcat的类加载机制吧。

二java类加载机制

1类加载器

类加载阶段中通过类的全限定名来获取描述此类的二进制字节流,这一动作放到java虚拟机外部实现。实现这一动作的代码模块称为类加载器。

2判断两个类相等

一个类在java虚拟机中的唯一性,是由加载它的类加载器和这个类本身共同确立的。也就是说:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

3双亲委派模式

Java 提供三种类型的系统类加载器:

  1. 启动类加载器(Bootstrap ClassLoader):由C++语言实现,属于JVM的一部分,其作用是加载 <JAVA_HOME>\lib 目录中的文件,或者被-Xbootclasspath参数所指定的路径中的文件,并且该类加载器只加载特定名称的文件(如 rt.jar),而不是该目录下所有的文件。启动类加载器无法被Java程序直接引用。
  2. 扩展类加载器(Extension ClassLoader):由sun.misc.Launcher.ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  3. 应用程序类加载器(Application ClassLoader):也称系统类加载器,由sun.misc.Launcher.AppClassLoader实现。负责加载用户类路径(Class Path)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。加载流程如下图所示:

注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。

这种类加载层级称为双亲委派模式:一个类加载器收到类加载请求后,首先不会自己加载而是把这个请求委派给父加载器完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

为什么要这样呢?

都知道java.lang.Object是java中所有类的父类,它存放在rt.jar之中,按照双亲委派模型,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。试想,如果没有使用双亲委派模型,由各个类加载器自行去加载,显然,这就存在很大风险,用户完全可以恶意编写一个java.lang.Object类,然后放到ClassPath下,那系统就会出现多个Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

详细可以参考这篇博文:

Java 类加载机制详解

4打破双亲委派模式

双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。

它很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么办?

Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器(Application ClassLoader)。

三tomcat类加载器

1web容器应该具备的特性

摘要:

隔离性:部署在同一web容器上的web应用程序所使用的java类库可以实现相互隔离

灵活性:web应用之间的类加载器相互独立,就能针对一个web应用进行重新部署。

性能:部署在同一web容器的两个web应用程序所使用的java类库可以共享

  • 隔离性:部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离。设想一下,两个Web应用,一个使用了Spring2.5,另一个使用了教新的4.0,应用服务器使用一个类加载器,Web应用将会因为jar包覆盖而无法启动。
  • 灵活性:Web应用之间的类加载器相互独立,那么就能针对一个Web应用进行重新部署,此时Web应用的类加载器会被重建,而且不会影响其他的Web应用。如果采用一个类加载器,类之间的依赖是杂乱复杂的,无法完全移出某个应用的类。
  • 性能:部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以互相共享。这个需求也很常见,例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到Web容器的内存,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。
  • ...

2tomcat类加载器结构

可以看到在原先的java类加载器基础上,tomcat新增了几个类加载器,包括3个基础类加载器和每个Web应用的类加载器,其中3个基础类加载器可在conf/catalina.properties中配置,具体介绍下:

  • Common:tomcat顶层的公用类加载器(负责tomcat应用服务器内部和web应用均可见的类),其路径由conf/catalina.properties中的common.loader指定,默认指向${catalina.home}/lib下的jar包
  • Catalina:加载tomcat应用服务器的类加载器(负责tomcat应用服务器内部可见,web应用不可见的类),其路径由server.loader指定,默认为空,此时tomcat使用Common类加载器加载应用服务器。
  • Shared:是所有Web应用的父类加载器(负责web应用共享类),其路径由shared.loader指定,默认为空,此时tomcat使用Common类加载器作为Web应用的父加载器。
  • Web应用:以Shared类加载器为父类,加载/WEB-INF/classes目录下的未压缩的Class和资源文件以及/WEB-INF/lib目录下的jar包,该类加载器只对当前Web应用可见,对其他Web应用均不可见

默认情况下,Common、Catalina、Shared类加载器是同一个,但可以配置3个不同的类加载器,使他们各司其职。

首先,Common类加载器复杂加载Tomcat应用服务器内部和Web应用均可见的类,如Servlet规范相关包和一些通用工具包。

其次,Catalina类加载器负责只有Tomcat应用服务器内部可见的类,这些类对Web应用不可见。比如,想实现自己的会话存储方案,而且该方案依赖了一些第三方包,当然是不希望这些包对Web应用可见,这时可以配置server.load,创建独立的Catalina类加载器。

再次,Shared类负责加载Web应用共享类,这些类tomcat服务器不会依赖。

相信看到这,引言中的疑问已经解开了吧。

那还有一个问题,如果有10个Web应用程序都是用Spring来进行组织和管理的话,可以把Spring放到Common或Shared目录下让这些程序共享。Spring要对用户程序的类进行管理,自然要能访问到用户程序的类,而用户的程序显然是放在/WebApp/WEB-INF目录中的,那么被CommonClassLoader或SharedClassLoader加载的Spring如何访问并不在其加载范围内的用户程序呢?

如果按主流的双亲委派机制,显然无法做到让父类加载器加载的类去访问子类加载器加载的类,但使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。spring加载类所用的Classloader是通过Thread.currentThread().getContextClassLoader()来获取的,而当线程创建时会默认setContextClassLoader(AppClassLoader),即线程上下文类加载器被设置为AppClassLoader,spring中始终可以获取到这个AppClassLoader(在Tomcat里就是WebAppClassLoader)子类加载器来加载bean,以后任何一个线程都可以通过getContextClassLoader()获取到WebAppClassLoader来getbean了。

问题扩展

  通过对上面tomcat类加载机制的理解,就不难明白 为什么java文件放在Eclipse中的src文件夹下会优先jar包中的class?

  这是因为Eclipse中的src文件夹中的文件java以及webContent中的JSP都会在tomcat启动时,被编译成class文件放在 WEB-INF/class 中。

  而Eclipse外部引用的jar包,则相当于放在 WEB-INF/lib 中。

  因此肯定是 java文件或者JSP文件编译出的class优先加载

  通过这样,我们就可以简单的把java文件放置在src文件夹中,通过对该java文件的修改以及调试,便于学习拥有源码java文件、却没有打包成xxx-source的jar包。

 

  另外呢,开发者也会因为粗心而犯下面的错误。

  在 CATALINA_HOME/lib 以及 WEB-INF/lib 中放置了 不同版本的jar包,此时就会导致某些情况下报加载不到类的错误。

  还有如果多个应用使用同一jar包文件,当放置了多份,就可能导致 多个应用间 出现类加载不到的错误。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值