类加载器在Tomcat中的应用

时间:2021-11-26 13:55:16

之前有文章已经介绍过了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>

类加载器在Tomcat中的应用的更多相关文章

  1. Java中的类加载器以及Tomcat的类加载机制

    在加载阶段,虚拟机需要完成以下三件事情: 1.通过一个类的全限定名来获取其定义的二进制字节流. 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构. 3.在Java堆中生成一个代表这个类 ...

  2. Tomcat源码分析(类加载与类加载器)

    Tomcat的挑战 Tomcat上可以部署多个项目 Tomcat的一般部署,可以通过多种方式启动一个Tomcat部署多个项目,那么Tomcat在设计时会遇到什么挑战呢? Tomcat运行时需要加载哪些 ...

  3. 4&period;自定义类加载器实现及在tomcat中的应用

    了解了类加载器的双亲委派机制, 也知道了双亲委派机制的原理,接下来就是检验我们学习是否扎实了,来自定义一个类加载器 一. 回顾类加载器的原理 还是这张图,类加载器的入口是c++调用java代码创建了J ...

  4. Java中的类加载器

    转载:http://blog.csdn.net/zhangjg_blog/article/details/16102131 从java的动态性到类加载机制   我们知道,Java是一种动态语言.那么怎 ...

  5. Tomcat类加载器

    1JVM类加载机制   JVM的ClassLoader通过Parent属性定义父子关系,可以形成树状结构.其中引导类.扩展类.系统类三个加载器是JVM内置的. 它们的作用分别是: 1)引导类加载器:使 ...

  6. Java类加载机制与Tomcat类加载器架构

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

  7. java中的类加载器ClassLoader和类初始化

    每个类编译后产生一个Class对象,存储在.class文件中,JVM使用类加载器(Class Loader)来加载类的字节码文件(.class),类加载器实质上是一条类加载器链,一般的,我们只会用到一 ...

  8. Java中的类加载器--Class loader

    学习一下Java中的类加载器,这个是比较底层的东西,好好学习.理解一下.  一.类加载器的介绍 1.类加载器:就是加载类的工具,在java程序中用到一个类,java虚拟机首先要把这个类的字节码加载到内 ...

  9. Tomcat 类加载器的实现

    Tomcat 内部定义了多个 ClassLoader,以便应用和容器访问不同存储库中的类和资源,同时达到应用间类隔离的目的.本文首发于公众号:顿悟源码. 1. Java 类加载机制 类加载就是把编译生 ...

随机推荐

  1. 使用dbms&lowbar;scheduler包创建定时任务

    本文使用dbms_scheduler的create_job创建执行存储过程的定时任务 创建一个job job_type指定'STORED_PROCEDURE' job_action中填入要执行的存储过 ...

  2. 诡异的问题&OpenCurlyDoubleQuote;该字符串未被识别为有效的 DateTime”

    问题描述:"该字符串未被识别为有效的 DateTime"这个异常相信大家都会碰到,但是这一次真的无法理解,服务器运行一段时间之后才会出现这个问题,并且是系统中所有和日期相关的模块, ...

  3. POJ 2182 Lost Cows &lpar;线段树&rpar;

    题目大意: 有 n 头牛,编号为 1 - n 乱序排成一列,现已知每头牛前面有多少头牛比它的编号小,从前往后输出每头牛的编号. 思路: 从后往前推,假如排在最后的一头牛比他编号小的数量为a,那么它的编 ...

  4. 201521123028 《Java程序设计》第13周学习总结

    本周学习总结 书面作业 网络基础 1.1 比较ping www.baidu.com与ping cec.jmu.edu.cn,分析返回结果有何不同?为什么会有这样的不同? Ans:cec.jmu.edu ...

  5. 后台模拟请求 HttpHelper 序列化 MD5 时间戳

    使用场景 C# 服务器后台调用其他服务器并处理 场景 /// <summary> /// 获取用户信息 /// </summary> /// <param name=&q ...

  6. 【Unity】微软的一款依赖注入组件

    前言 前面学习了autofac这个依赖注入组件,本来是打算写在一起的,因为这个组件没打算像autofac一样详细的写,只是写下以前自己鼓捣玩搭建框架然后使用的一个依赖注入组件,并且也是进行了封装使用. ...

  7. Difference between Load &sol; Stress &sol; Performance Testing

    Load and stress testing are subsets of performance testing. Performance testing means how best somet ...

  8. CentOS7 cannot find a valid baseurl for repo base

    找到文件夹: cd /etc/sysconfig/network-scripts/ 然后找ifcfg 开头的文件,挨个打开 里面有下面那些代码前三行的就是,添加后面DNS两行,保持并退出,然后继续执行 ...

  9. Quartz&period;Net进阶之四:CronTrigger 详述

    以前都是将所有的内容放在一篇文章里,就会导致文章很长,对于学习的人来说,有时候这也是一个障碍.所以,以后我的写作习惯,我就会把我写的文章缩短,但是内容不会少,内容更集中.这样,学习起来也不会很累,很容 ...

  10. &gt&semi;&gt&semi;&gt&semi;&gt&semi;&gt&semi;&gt&semi;&gt&semi;&gt&semi; &lbrack;ovs&rsqb;&lbrack;libvirt&rsqb; virt-xml ovs-vsctl

    查看可用的参数: [root@vrouter1 tong]# virt-xml --add-device --network=? |grep source source source_mode sou ...