竹笋

首页 » 问答 » 问答 » Tomcat源码分析Tomcat类加
TUhjnbcbe - 2024/2/26 22:30:00
甘露聚糖肽注射液 https://m-mip.39.net/czk/mipso_4329292.html

本章结构如下:

前言

Java类加载机制

tomcat类加载器

tomcat类加载器源码分析

一、前言

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

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

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

二、Java类加载机制

在这之前,当然要先了解一下java中类加载时怎样的,毕竟tomcat是用java写的,它的加载机制也是基于java的类加载机制。

2.1、类加载器

1.什么是类加载器?

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

2.如何判断两个类是否相等?

类加载器用于实现类的加载动作。对于任意一个类,都需要由加载它的类加载器和这个类本身共同确立其在Java虚拟机中的唯一性,每一个类,都拥有一个独立的类名称空间。也就是说:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

2.2、双亲委派模型

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

启动类加载器(BootstrapClassLoader):由C++语言实现,属于JVM的一部分,其作用是加载<JAVA_HOME>\lib目录中的文件,或者被-Xbootclasspath参数所指定的路径中的文件,并且该类加载器只加载特定名称的文件(如rt.jar),而不是该目录下所有的文件。启动类加载器无法被Java程序直接引用。

扩展类加载器(ExtensionClassLoader):由sun.misc.Launcher.ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

应用程序类加载器(ApplicationClassLoader):也称系统类加载器,由sun.misc.Launcher.AppClassLoader实现。负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

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

用一段代码测试一下:

publicstaticvoidmain(String[]args){ClassLoaderloader=Xxx.class.getClassLoader();while(loader!=null){System.out.println(loader);loader=loader.getParent();}}

结果:

从结果我们可以看出,默认情况下,用户自定义的类使用AppClassLoader加载,AppClassLoader的父加载器为ExtClassLoader,但是ExtClassLoader的父加载器却显示为空,这是什么原因呢?究其缘由,启动类加载器属于JVM的一部分,它不是由Java语言实现的,在Java中无法直接引用,所以才返回空。

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

为什么要这样呢?

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

2.3、简单看下源码

protectedClass?loadClass(Stringvar1,booleanvar2)throwsClassNotFoundException{synchronized(this.getClassLoadingLock(var1)){//首先,检查请求的类是否已经被加载过了Classvar4=this.findLoadedClass(var1);if(var4==null){longvar5=System.nanoTime();try{if(this.parent!=null){var4=this.parent.loadClass(var1,false);}else{var4=this.findBootstrapClassOrNull(var1);}}catch(var10){//如果父类加载器抛出ClassNotFoundException//说明父类加载器无法完成加载请求;}if(var4==null){longvar7=System.nanoTime();//在父类加载器无法加载的时候//再调用本身的findClass方法来进行类加载var4=this.findClass(var1);PerfCounter.getParentDelegationTime().addTime(var7-var5);PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);PerfCounter.getFindClasses().increment();}}if(var2){this.resolveClass(var4);}returnvar4;}}

从源码可以看出,ExtClassLoader和AppClassLoader都继承自ClassLoader类,ClassLoader类中通过loadClass方法来实现双亲委派机制。整个类的加载过程可分为如下三步:

查找对应的类是否已经加载。

若未加载,则判断当前类加载器的父加载器是否为空,不为空则委托给父类去加载,否则调用启动类加载器加载(findBootstrapClassOrNull再往下会调用一个native方法)。

若第二步加载失败,说明父类加载器无法完成加载请求,则调用当前类加载器加载。

详细可以参考这篇博文:

Java类加载机制详解

2.4、打破双亲委派模型(来自《深入理解Java虚拟机:JVM高级特性与最佳实践》)

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

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

这并非是不可能的事情,一个典型的例子便是JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI,ServiceProviderInterface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?

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

有了线程上下文类加载器,就可以做一些“舞弊”的事情了,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。

还有“被破坏”是由于用户对程序的动态性的追求导致的,例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。

当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:1)将以java.*开头的类委派给父类加载器加载。2)否则,将委派列表名单内的类委派给父类加载器加载。3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。5)否则,查找类是否在自己的FragmentBundle中,如果在,则委派给FragmentBundle的类加载器加载。6)否则,查找DynamicImport列表的Bundle,委派给对应Bundle的类加载器加载。7)否则,类查找失败。

三、tomcat类加载器

了解了java的双亲委派模型,现在回到正题上,tomcat的类加载器是怎么样的?

3.1、Web容器应该具备的特性

不难想象,一个功能健全的Web容器,它的类加载器必然有多个,因为它应该具备如下特性:

隔离性:部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离。设想一下,两个Web应用,一个使用了Spring2.5,另一个使用了教新的4.0,应用服务器使用一个类加载器,Web应用将会因为jar包覆盖而无法启动。

灵活性:Web应用之间的类加载器相互独立,那么就能针对一个Web应用进行重新部署,此时Web应用的类加载器会被重建,而且不会影响其他的Web应用。如果采用一个类加载器,类之间的依赖是杂乱复杂的,无法完全移出某个应用的类。

性能:部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以互相共享。这个需求也很常见,例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到Web容器的内存,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。

...

3.2、tomcat类加载器结构

了解了一款Web容器应该具备的特性,明白了Web容器的类加载器有多个,再来看tomcat的类加载器结构。

首先上张图,整体看下tomcat的类加载器:

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

Common:以应用类加载器为父类,是tomcat顶层的公用类加载器,其路径由conf/catalina.properties中的

1
查看完整版本: Tomcat源码分析Tomcat类加