tomcat的类加载机制

       跟其他主流的Java Web服务器一样,Tomcat也拥有不同的自定义类加载器,达到对各种资源库的控制。一般来说,Java Web服务器需要解决以下四个问题:

①   同一个Web服务器里,各个Web项目之间各自使用的Java类库要互相隔离。(WebAppClassLoader加载)

②   同一个Web服务器里,各个Web项目之间可以提供共享的Java类库。(CommonClassLoader加载

③   服务器为了不受Web项目的影响,应该使服务器的类库与应用程序的类库互相独立。(SystemClassLoader加载)

④   对于支持JSP的Web服务器,应该支持热插拔(hotswap)功能。(JspClassLoader加载)

对于以上几个问题,如果单独使用一个类加载器明显是达不到效果的,必须根据实际使用若干个自定义类加载器。这就是为什么需要定义这么多类加载器的原因。

下面以本书主要剖析的Tomcat7为例,看看它的类加载器是怎样定义的?如图2-4-3,启动类加载器、扩展类加载器、应用程序类加载器这三个类加载器数据JDK级别的加载器,他们是唯一的,我们一般不会对其做任何更改。接下来则是Tomcat的类加载器,在tomcat7中,最重要的一个类加载器是CommonClassLoader,它的父类加载器是ApplicationClassLoader(即SystemClassLoader),负责加载 $CATALINA_BASE/lib、  $CATALINA_HOME/lib两个目录下所有的.class跟.jar文件。而下面虚线框的两个类加载器有必要说明一下,如果在Tomcat5版本,这两个类加载器实例默认与Common ClassLoader实例不同,Common ClassLoader为他们的父类加载器。而在Tomcat7中,这两个实例变量也存在,只是catalina.properties配置文件没有对server.loader跟share.loader两项进行配置,所以在程序里,这两个类加载器实例就被赋值为CommonClassLoader实例,即一个tomcat实例其实就只有CommonClassLoader实例。如下类加载结构图:


  • CommonClassLoader:加载的类目录通过{tomcat}/conf/catalina.properties中的common.loader指定,以SystemClassLoader为parent(目前默认定义是common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar)
  • CatalinaClassLoader:加载的类目录通过{tomcat}/conf/catalina.properties中server.loader指定,以CommonClassLoader为parent,如果server.loader配置为空,则ServerClassLoader 与CommonClassLoader是同一个(默认server.loader配置为空)
  • SharedClassLoader:加载的类目录通过{tomcat}/conf/catalina.properties中share.loader指定,以CommonClassLoader为parent,如果server.loader配置为空,则SharedClassLoader与CommonClassLoader是同一个(默认share.loader配置为空)
  • WebAppClassLoader:可包含多个。每个Context一个WebAppClassLoader实例,负责加载context的/WEB-INF/lib和/WEB-INF/classes目录,优先搜索/WEB-INF/classes下面的。context间的隔离就是通过不同的WebAppClassLoader来做到的。由于类定义一旦加载就不可改变,因此要实现tomcat的context的reload功能,实际上是通过新建一个新的WebAppClassLoader来做的,因此reload的做法实际上代价是很高昂的,需要注意的是,JVM内存的Perm区是只吃不拉的,因此抛弃掉的WebAppClassLoader加载的类并不会被JVM释放,因此tomcat的reload功能如果应用定义的类比较多的话,reload几次就OutOfPermSpace异常了。
  • JasperLoader:可包含多个每个JSP一个JasperLoader实例,与WebAppClassLoader做法类似,JSP支持修改生效是通过丢弃旧的JasperLoader,建一个新的JasperLoader来做到的,同样的,存在轻微的PermSpace的内存泄露的情况

以上对个个classloader的作用做了介绍,但请读者不要搞混淆了,上边说的个个类加载器只是类加载器的名字,不是类加载类的名字。

对照这样的一个类加载器结构,看看上面Java Web服务器需要解决的问题是否解决。由于每个Web应用项目都有自己的WebApp ClassLoader,很好地使多个Web应用程序之间互相隔离,彼此不能访问对方的类文件。并且能有效使Tomcat不受Web应用程序影响;Common ClassLoader的存在使多个Web应用程序能够互相共享类库;而每一个JSP文件对应一个Jsp ClassLoader则可以使Tomcat支持热替换功能。

下面从代码层面进行分析,如下图所示,commomloader的父类加载器是AppClassLoader:


tomcat服务器启动时需要依赖jdk核心库,这些代码是由jdk本身的类加载器加载的。commonLoader本质上就是一个URLClassLoader的实例。通过委托机制,从而得出它的父类加载器是AppClassLoader。此父类加载器不是我们所熟知的继承机制。

由于server.loader和shared.loader的配置为空,所以其实commonLoader、catalinaLoader和sharedLoader都是指向同一个类加载器实例,看代码如下:(限于篇幅只贴部分代码)

private ClassLoader createClassLoader(String name, ClassLoader parent)
       throws Exception {
       String value = CatalinaProperties.getProperty(name + ".loader");
       if ((value == null) || (value.equals("")))
           return parent;
- Bootstrap - 载入JVM自带的类和$JAVA_HOME/jre/lib/ext/*.jar 
- System - 载入$CLASSPATH/*.class 。环境变量CLASSPATH配置的路径下:.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tool.jar;

- Common - 载入$CATALINA_HOME/common/...,它们对TOMCAT和所有的WEB APP都可见 。
  - Catalina - 载入$CATALINA_HOME/server/...,它们仅对TOMCAT可见,对所有的WEB APP都不可见 。tomcat6以后均由Common加载。
  - Shared - 载入$CATALINA_HOME/shared/...,它们仅对所有WEB APP可见,对TOMCAT不可见(也不必见) 。tomcat6以后均由Common加载。
  - WebApp - 载入$Context_Base/WEB-INF/...,它们仅对该WEB APP可见 。加载各自的web应用程序。

ClassLoader 的工作原理

每个运行中的线程都有一个成员contextClassLoader,用来在运行时动态地载入其它类 系统默认的contextClassLoader是systemClassLoader,所以一般而言Java程序在执行时可以使用JVM自带的类、$JAVA_HOME/jre/lib/ext/中的类和$CLASSPATH/中的类。不过 可以使用 Thread.currentThread().setContextClassLoader(...);更改当前线程的contextClassLoader,来改变其载入类的行为 。

ClassLoader 被组织成树形,一般的工作原理是: 
1) 线程需要用到某个类,于是contextClassLoader被请求来载入该类 。
2) contextClassLoader请求它的父ClassLoader来完成该载入请求 。bootstrapClassLoader搜索$JAVA_HOME/jre/lib/rt.jar,extClassLoader搜索$JAVA_HOME/jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。SystemClassLoader搜索CLASSPATH制定下的目录(获取通过jvm参数指定的目录)。
3) 如果父ClassLoader无法载入类,则contextClassLoader试图自己来载入 。如CommonClassLoader,当它委托父类都无法加载的时候,则由CommonClassLoader加载,然后它搜索指定的目录去加载类文件。

类的委托机制,其实质就是每个级别的类加载器都只是负责加载自己权限范围内的类文件。

注意 :WebAppClassLoader的工作原理和上述有少许不同: 它先试图自己载入类(在$Context_Base/WEB-INF/...中载入类),如果无法载入,再请求父ClassLoader完成 。

由此可得: 
- 对于WEB APP线程,它的contextClassLoader是WebAppClassLoader 
- 对于Tomcat Server线程,它的contextClassLoader是CatalinaClassLoader 

另一篇博文:

说到本篇的tomcat类加载机制,不得不说翻译学习tomcat的初衷。

  之前实习的时候学习javaMelody的源码,但是它是一个Maven的项目,与我们自己的web项目整合后无法直接断点调试。后来同事指导,说是直接把java类复制到src下就可以了。很纳闷....为什么会优先加载src下的java文件(编译出的class),而不是jar包中的class呢?

现在了解tomcat的类加载机制,原来一切是这么的简单。


JVM中的类加载机制

在JVM中并不是一次性把所有的文件都加载到,而是一步一步的,按照需要来加载。

  比如JVM启动时,会通过不同的类加载器加载不同的类。当用户在自己的代码中,需要某些额外的类时,再通过加载机制加载到JVM中,并且存放一段时间,便于频繁使用。

JVM类加载采用 父类委托机制,如下图所示:


JVM中包括集中类加载器:

  1 BootStrapClassLoader 引导类加载器,加载rt.jar。创建JVM环境所需。比如定义各种数据类型int,long....等

  2 ExtClassLoader 扩展类加载器。加载jdk/jre/lib/ext/下面的jar包

  3 AppClassLoader 应用类加载器。

  4 CustomClassLoader 用户自定义类加载器。

  他们的区别上面都有说明。需要注意的是,不同的类加载器加载的类是不同的,因此如果用户加载器1加载的某个类,其他用户并不能够使用。类在JVM的唯一性是由类加载器和Class的类路径唯一确定的。意思就是如果加载器不同,Class的类路径相同,加载到JVM中的Class对象都是不同的。

  当JVM运行过程中,用户需要加载某些类时,会按照下面的步骤(父类委托机制

  1 用户自己的类加载器,把加载请求传给父加载器,父加载器再传给其父加载器,一直到加载器树的顶层。

  2 最顶层的类加载器首先针对其特定的位置加载,如果加载不到就转交给子类。

  3 如果一直到底层的类加载都没有加载到,那么就会抛出异常ClassNotFoundException。

  因此,按照这个过程可以想到,如果同样在CLASSPATH指定的目录中和自己工作目录中存放相同的class,会优先加载CLASSPATH目录中的文件。

Tomcat的类加载

在tomcat中类的加载稍有不同,如下图:


当tomcat启动时,会创建几种类加载器:

  1 Bootstrap 引导类加载器 

  加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下)

  2 System 系统类加载器 

  加载tomcat启动的类,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下。



通过对上面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包文件,当放置了多份,就可能导致 多个应用间 出现类加载不到的错误。

3 Common 通用类加载器 

  加载tomcat使用以及应用通用的一些类,位于CATALINA_HOME/lib下,比如servlet-api.jar


4 webapp 应用类加载器

  每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件

  当应用需要到某个类时,则会按照下面的顺序进行类加载

  1 使用bootstrap引导类加载器加载

  2 使用system系统类加载器加载

  3 使用应用类加载器在WEB-INF/classes中加载

  4 使用应用类加载器在WEB-INF/lib中加载

  5 使用common类加载器在CATALINA_HOME/lib中加载

问题扩展

通过对上面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包文件,当放置了多份,就可能导致 多个应用间 出现类加载不到的错误。


展开阅读全文

没有更多推荐了,返回首页