阅读 131

类加载器在Tomcat中的应用

之前有文章已经介绍过了JVM中的类加载机制,JVM中通过类加载加载class文件,通过双亲委派模型完成分层加载。实际上类加载机制并不仅仅是在JVM中得以运用,通过影响字节码生成和类加载器目前已经有了许多相关的技术诞生。特别的对于进行应用服务器的开发过程中,类加载机制几乎是必须掌握的。

为什么在Tomcat中需要自定义类加载器

做Java开发的肯定都有用过tomcat,回想一下我们使用tomcat是的场景。最初的时候使用tomcat大多都是单纯的使用其作为项目的容器,而没有考虑多着中间的很多问题。

隔离

最初我们使用都是在一台tomcat上部署多个项目,然后通过端口进行区分。那么这里就存在着一个问题,我们都知道一台tomcat会启动一个JVM,部署在tomcat 的应用实际上是跑在这一个jvm中的。

相信大家多多少少都遇到过jar包冲突的问题。不同的项目会有不同的依赖jar包,可能是不同类型的包,也可能是相同包的不同版本问题。前一文章中又提到jvm校验类的唯一性是通过类加载器+全限定名完成的。如果只是前文中提到的几中类加载器的话那么在加载多个项目时肯定会出现不同版本的jar包覆盖的情况,那么程序一跑起来就报抛出ClassNotFoundException的异常。

那么很明显tomcat需要对不同的项目进行隔离,保证各个项目不会相互影响,这正好可以通过类加载器来完成。

共享

上面讲到了不同项目间需要进行隔离,而除了隔离还需要考虑共享的问题。Java开发中其臃肿的体系一直为人诟病,一个很小的web一个项目会有这一大堆的依赖包。如果在一台tomcat中部署多个项目,每个项目都需要单独将依赖类加载到JVM中。这样导致的后果一是占据了一部分存储空间;二是消耗了大量的内存,每个项目都单独加载一次,在启动tomcat后可能会存在大量重复的Class对象(这里的重复不是JVM意义上的重复,只是说来源于同一个包),那么项目运行可用内存变少使得GC变得频繁。

解决这个问题我们可以将多个项目中共同依赖的包抽出来让JVM只加载一次。这样既不会占据多的存储空间,对内存的消耗也将减少。

性能

tomcat作为一个应用服务器性能肯定是非常需要考虑的事情,我们都知道JVM在加载Class文件时是没有办法直接定位到一个具体的文件的,只能搜索指定目录下的的jar包的内容,那么这里的问题就是如果很多的web项目依赖了大量的jar文件,这时要加载一个Class文件就可能会搜索这所有的jar才能加载到该类,这样对性能的影响就很大了。

HotSwap

hotSwap也叫热加载,之前在查找资料的时候发现不少文章都没弄清楚热加载和热部署的区别,这里简单说笑。

/ 热部署 热加载
实现方式 发生修改时整个项目重新打包部署 只替换被修改过的类
使用场景 生产可以使用 开发时使用,因为开发中需要频繁的调试代码

热部署是在当代码被修改后重新打包整个项目后重新部署,实现方式有多种,目前很多应用服务器都有这个功能,还有一些第三方工具也可以做到。但是不建议在开发中打开该功能,很多人不明白热部署和热加载的区别,结果在开发时使用了热部署结果导致可能没修改一次代码然后就后台跑一个打包部署的程序,使得电脑变得很卡。

热替换的典型应用就是对于JSP应用的运行了。我们都知道JSP应用运行是先将JSP翻译为Servlet.java,然后将.java编译为.class文件。同时由于jsp中将逻辑代码和页面放在一起了,其修改的概率很高,如果每次修改都需要手动重启项目那会严重影响开发效率,但如果使用热部署确实能解决问题,但也会电脑卡死的风险。所以我们希望的是在修改后能够只重新加载被修改过的文件,其他没有修改的不懂。

需要哪些类加载器

通过上面的问题我们可以大概的整理出一个tomcat中的类加载模型。

首先JVM中已经定义好的几个类加载器肯定是不能少的。

如何实现隔离性?

要实现隔离我们得先理清哪些地方需要进行隔离。

首先部署的不同的项目之间肯定是需要进行隔离的,防止出现包覆盖,各个项目相互之间要隔离那肯定就需要每个项目有一个独属于自己的类加载器,这样才能够保证类加载器+全限定名的唯一性。

其次呢我们知道tomcat本身也是Java语言进行开发的,那么它本身肯定也会有依赖的jar包,那么加载tomcat依赖也需要一个类加载器。为什么加载tomcat依赖不能使用JVM提供的类加载器呢?因为假如使用了JVM的类加载器加载了jar包后,如果其他项目有依赖相同的jar,那么根据双亲委派模型又会存在包覆盖的问题了。

到这里我们知道了每个web项目需要一个类加载器,加载tomcat依赖包也需要一个类加载器。

如何实现共享性?

为了利用好JVM的内存,共享依赖的jar也是很有必要的,在实现隔离性时对每一个web项目都准备了一个类加载器。而共享是要求在加载共享的这一部分jar文件时不从当前项目中加载,而是使用共享的jar文件。

加载共享的jar肯定也需要一个类加载器,根据其使用的特性来说加载共享jar的类加载器和web项目的类加载器还存在着一个层级关系。

热部署和热加载?

热部署是重新加载整个项目,那就很明朗了,如果重新加载那就代表以前加载的就失效了,我们可以直接废弃掉之前项目的类加载器,tomcat重新生成一个新的加载当前项目的类加载器即可。

热加载就麻烦点了,热加载要求在修改文件后不重新打包部署项目也能够直接使用。那么直接重新加载整个项目就不可取了。

一种方法是首先卸载掉被修改的文件咋JVM中已经存在的Class,然后重新加载修改后的文件。理论上来说这是可行的,但是实际上Class的卸载条件本身及其苛刻,而且Class的卸载时有GC完成的,我们没有办法主动的完成卸载的这个过程,所以这一方法就不可行了。

另一个方式就是给每一个jsp创建一个类加载器,这个类加载器只负责这一个jsp,当文件被修改后将这一个类加载器无效,然后重新创建一个新的类加载器来加载即可。

而对每一个web项目都创建一个类加载器使得在加载时也不会去从别的项目中搜索jar,就不会存在每次加载好事很久的问题了。

类加载结构

根据上面的分析我们可以得出一个tomcat中的类加载结构。

自定义tomcat类加载结构

最上层是由JVM提供的几个默认的类加载器,tomcat类加载器来加载tomcat本身需要的依赖包,共享jar类加载器加载在不同项目间共同依赖的jar。

实际的tomcat的类加载结构如下图:

tomcat类加载结构

想较与我们自己设计的多了一个common Classloader,在默认情况下我们并不会使用到共享的功能,基本上都是由web类加载器来加载整个项目,所以默认情况下tomcat都不开启shared classloader和Catalina classloader,不开启的情况下默认都将所有这部分功能有common classloader代替。

破坏双亲委派模型

这里有一个问题需要讨论的是tomcat这种类加载模型是否破坏了双亲委派模型呢?

答案是肯定的,要实现上面的功能,那么根据jar的功能其肯定是被其指定的classloader进行加载而不会继续往上推(比如共享的jar在Shared Classloader就直接加载了而不会继续推向Common Classloader)。这明显是不符合双亲委派模型的。

而且在tomcat的类加载模型中,假设一部分被共享的jar由Shared Classloader进行加载(比如spring)。我们都知道spring作为一个ioc容器必然是需要访问到我们web应用中的类的,如果要在spring中加载需要的类这时使用的classloader就是加载spring的classloader,而用户程序显然是放在/WEB-INF目录中的,加载该目录的classloader却是web classloader。在上面的类加载层级关系中我们可以看到这两个加载器是上下级关系,但是双亲委派模型中要求向上查找而不能向下查找,那么很明显如果遵循双亲委派模型的话功能就无法正常运行了。

解决这个问题也很简单,JVM团队提供了一个线程上下文类加载器( Thread Context Class Loader)。这个类加载器可以通过Thread类的setContextClassLoaserO方法进行设置,使用该方法就可以让父类加载器请求子类加载器去完成类加载的动作,这也会打破双亲委派模型的层次结构来逆向使用类加载器。

解决上面问题就是通过spring使用线程上下文加载器来加载类,而线程上下文加载器默认设置为了WebAppClassLoader,那么这时是哪一个Web应用调用了spring,spring就会用该应用的WebAppClassLoader来加载需要的bean。

如何使用

Tomcat默认情况下仅使用common classloader来加载自身的依赖和共享的依赖。如果我们要启用Catalina Classloader或者Shared Calssloader需要自己进行手动配置。

下图为Tomcat8.0的目录结构:

Tomcat目录结构

默认情况下Common ClassLoader加载的jar都放在lib包下。

如果我们要进行配置需要找到conf/catalina.properties文件。找到common.loader,server.loader,shared.loader三个参数,这三个参数分别设置Common ClassLoader,Catalina ClassLoader,Shared ClassLoader的加载的jar包的位置。

参数值可以是一个指定目录下的所有包,可以是指定的jar包,也可以是包名符合一定规则的jar包。

该配置文件还可以配置哪些指定名称的jar包不进行加载。

热加载or热部署

要想使用热加载或者热部署也需要修改配置文件,在conf/Catalina/localhost文件夹下新建一个xml文件,设置内容为:

//热加载
//docBase指项目路径,可以使用绝对路径或相对路径,相对路径是相对于webapps 
<Context docBase="D:\demo\WebRoot" path="/demo" reloadable="true"/>
复制代码
//热部署
//热部署只需要将reloadable置为false即可,这里存在一个属性autoDeploy默认为true,表示支持热部署
<Context docBase="D:\demo\WebRoot" path="/demo" reloadable="false"/>
复制代码

其实并不一定都需要添加新的xml文件,我们也可以找到conf/server.xml中在下添加标签,值和这里一致。

//<Host>标签中autoDeploy=true,表示默认支持热部署,
//这样只要tomcat在运行中,我们将war包放入到webapps下tomcat就会帮我们自动的部署
<Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
            </Host>
复制代码

link