Tomcat的挑战
Tomcat上可以部署多个项目
Tomcat的一般部署,可以通过多种方式启动一个Tomcat部署多个项目,那么Tomcat在设计时会遇到什么挑战呢?
Tomcat运行时需要加载哪些类
Tomcat中的多个项目可能存在相同的类
Tomcat中类加载的挑战
源码分析彻底弄懂Tomcat的类加载
类加载与类加载器
类加载
类加载:主要是将.class文件中的二进制字节读入到JVM中
我们可以看到因为这个定义,所以并没有规定一定是要磁盘加载文件,可以通过网络,内存什么的加载。只要是二进制流字节数据,JVM就认。
类加载过程:
1.通过类的全限定名获取该类的二进制字节流;
2.将字节流所代表的静态结构转化为方法区的运行时数据结构
3.在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
类加载器
定义:JVM设计者把“类加载”这个动作放到java虚拟机外部去实现,以便让应用程序决定如何获取所需要的类。实现这个动作的代码模块成为“类加载器”
类与类加载器
对于任何一个类,都需要由加载它的类加载器和这个类来确定其在JVM中的唯一性。也就是说,两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。
注意:这里所谓的“相等”,一般使用instanceof关键字做判断。
类加载器与双亲委派模型
类加载器
启动类加载器:该加载器使用C++语言实现,属于虚拟机自身的一部分。
启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOMElib目录中并且能被虚拟机识别的类库加载到JVM内存中,如果名称不符合的类库即使在lib目录中也不会被加载。该类加载器无法被java程序直接引用
拓展类加载器与应用程序类加载器:另一部分就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承抽象类java.lang.ClassLoader。
扩展类加载器(Extension ClassLoader):该加载器主要负责加载JAVA_HOMElibext目录中的类库,开发者可以使用扩展加载器。
应用程序类加载器(Application ClassLoader):该列加载器也称为系统加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
双亲委派模型
定义:双亲委派模型的工作过程为:如果一个类加载器收到了类请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一层都是如此,因此所有类加载的请求都会传到启动类加载器,只有当父加载器无法完成该请求时,子加载器才去自己加载。
实现方式:该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器不是以继承的关系来实现,而是通过组合关系来复用父加载器的代码。
意义:好处双亲委派模型的好处就是java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如:Object,无论那个类加载器去加载该类,最终都是由启动类加载器进行加载的,因此Object类在程序的各种类加载环境中都是一个类。如果不用改模型,那么java.lang.Object类存放在classpath中,那么系统中就会出现多个Object类,程序变得很混乱。
双亲委派模型
从虚拟机的角度来说,有两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该加载器使用C++语言实现,属于虚拟机自身的一部分。另一部分就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承抽象类java.lang.ClassLoader.
从java开发人员的角度看,大部分java程序会用到以下三种系统提供的类加载器:
1、启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOMElib目录中并且能被虚拟机识别的类库加载到JVM内存中,如果名称不符合的类库即使在lib目录中也不会被加载。该类加载器无法被java程序直接引用。
2、扩展类加载器(Extension ClassLoader):该加载器主要负责加载JAVA_HOMElibext目录中的类库,开发者可以使用扩展加载器。
3、应用程序类加载器(Application ClassLoader):该列加载器也称为系统加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
JVM中的类加载器源码分析
AppClassLoader
URLClassLoader
SecureClassLoader
Tomcat中的类加载解决方案
Tomcat类加载的考虑
隔离性
Web应用类库相互隔离,避免依赖库或者应用包相互影响,比如有两个Web应用,一个采用了Spring 4,一个采用了Spring 5,而如果如果采用同一个类加载器,那么Web应用将会由于jar包覆盖而无法启动成功。
灵活性
因为隔离性,所以Web应用之间的类加载器相互独立,如果一个Web应用重新部署时,该应用的类加载器重新加载,同时不会影响其他web应用。
比如:不需要重启Tomcat的创建xml文件的类加载,
还有context元素中的reloadable字段:如果设置为true的话,Tomcat将检测该项目是否变更,当检测到变更时,自动重新加载Web应用。
性能
由于每一个Web应用都有一个类加载器,所以在Web应用在加载类时,不会搜索其他Web应用包含的Jar包,性能自然高于只有一个类加载的情况。
Tomcat中的类加载器
Tomcat提供3个基础类加载器(common、catalina、shared)和Web应用类加载器。
Tomcat中的类加载源码分析
三个基础类加载器
介绍
3个基础类加载器的加载路径在catalina.properties配置,默认情况下,3个基础类加载器的实例都是一个。
createClassLoader调用ClassLoaderFactory属于一种工厂模式,并且都是使用URLClassLoader
默认情况三个是一个实例,但是可以通过修改配置创建3个不同的类加载机制,使它们各司其职。
举个例子:如果我们不想实现自己的会话存储方案,并且这个方案依赖了一些第三方包,我们不希望这些包对Web应用可见,因此我们可以配置server.loader,创建独立的Catalina类加载器。
共享性:
Tomcat通过Common类加载器实现了Jar包在应用服务器与Web应用之间的共享,
通过Shared类加载器实现了Jar包在Web应用之间的共享
通过Catalina类加载器加载服务器依赖的类。
类加载工厂
因为类加载需要做很多事情,比如读取字节数组、验证、解析、初始化等。而Java提供的URLClassLoader类能够方便的将Jar、Class或者网络资源加载到内存中。而Tomcat中则用一个工厂类,ClassLoaderFactory把创建类加载器的细节进行封装,可以通过它方便的创建自定义类加载器。
使用加载器工厂的好处
- ClassLoadFactory有一个内部Repository,它就是表示资源的类,资源的类型用一个RepositoryType的枚举表示。
- 同时我们看到,如果在检查jar包的时候,如果有检查的URL地址的如果检查有异常就忽略掉,可以确保部分类加载正确。
尽早设置线程上下文类加载器
每一个运行线程中有一个成员ContextClassLoader,用于在运行时动态载入其他类,当程序中没有显示声明由哪个类加载器去加载哪个类(比如new出一个类时),将默认由当前线程类加载器加载,所以一般系统默认的ContextClassLoad是系统类加载器。
一般在实际的系统上,使用线程上下文类加载器,可以设置不同的加载方式,这个也是Java灵活的类加载方式的体现,也可以很轻松的打破双亲委派模式,同时也会避免类加载的异常。
Webapp类加载器
每个web应用会对一个Context节点,在JVM中就会对应一个org.apache.catalina.core.StandardContext对象,而每一个StandardContext对象内部都一个加载器实例loader实例变量。这个loader实际上是WebappLoader对象。而每一个 WebappLoader 对象内部关联了一个 classLoader 变量(就这这个类的定义中,可以看到该变量的类型是org.apache.catalina.loader.WebappClassLoader)。
所以,这里一个web应用会对应一个StandardContext 一个WebappLoader 一个WebappClassLoader 。
一个web应用对应着一个StandardContext实例,每个web应用都拥有独立web应用类加载器(WebappClassLoader),这个类加载器在StandardContext.startInternal()中被构造了出来。
注意这里:设置加载器和获取加载器都使用了读写锁机制,确保多线程情况下对共享资源的访问不会出现问题。
同时因为Tomcat的生命周期管理,必定会调用WebappLoader.java的startInternal()方法,该方法中new出了
所以总结一句话,如果没有弄懂Tomcat的启动流程,以及弄懂Tomcat的生命周期的管理,很多地方的源码是没有办法没有看懂,所以看源码也有一个先后顺序。
热加载源码分析
当配置信息中有reloadable的属性,并且值为true时,Tomcat怎么去完成这个过程呢?
还是看源码,据Tomcat的启动流程,我们分析下Context的初始化start方法,根据之前的课程我们可知道,Context只是一个接口,具体实现类是StandardContext,我们分析下startInternal方法(此方法由之前的抽闲骨架类中的start方法触发)
我发现有一个线程启动的方法 threadStart(),
由上图我们知道,这个是父类中调用,