打赏

Tomcat系列(7)——Tomcat类加载机制

1. 核心部分

1. 类加载器:

  通过一个类的全限定名来获取描述此类的二进制字节流。

  对于任意一个类,都需要由加载他的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。

2. 类加载机制:

  按需加载。

  Java虚拟机把描述类的数据从Class文件加载进内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型

3. JVM双亲委派模式

  JVM启动时,会通过不同的类加载器加载不同的类。

  1 BootStrapClassLoader 引导类加载器

  2 ExtClassLoader 扩展类加载器

  3 AppClassLoader 应用类加载器

  4 CustomClassLoader 用户自定义类加载器

4. Tomcat违反双亲委派模式

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

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

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

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

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

 

2. 类加载器

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

  类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载他的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。这句话可以表达的更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这个两个类就必定不相等。

3. 类加载机制

  Java虚拟机把描述类的数据从Class文件加载进内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

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

  

  1. 加载:通过这个类的全限定名找到这个类所在的位置,把它从一个文件或者一个字节流转化为虚拟机内存中的一个确确实实的对象
  2. 验证:验证是为了检查每个java文件所对应的class文件所形成的字节流中包含的信息符不符合虚拟机的要求,有没有危害虚拟机自身安全的代码。
  3. 准备:准备阶段就是为类变量(static修饰)分配内存,并设置初始值(例如int为0)
  4. 解析:解析就是将常量值的引用替换为实际值的过程
  5. 初始化:初始化是类加载的最后一步,在初始化阶段才开始真正执行类中所定义的java代码,注意这个初始化要和我们平时构造方法初始化(构造方法是在“使用”阶段用new关键字创建实例的时候才会调用)区分开来,这个初始化动作会把类中所有用了static修饰的变量以及静态语句块执行一遍,按照我们的意愿把类变量赋给我们所定义的变量。
  6. 使用:就是我们平时在编码过程中用new关键字去创建一个类的实例去使用这个类
  7. 卸载:虚拟机通过垃圾回收机制将这个类的信息和这个类相关的实例从虚拟机内存区域中移除。

  自定义的类加载器常常用于部署时的热替换,还有类的加密和解密。在部署程序的时候可能会把我们的class文件进行加密,在实际运行的时候首先要把加密后的class文件通过自定义的类加载器进行解密然后交给虚拟机去执行。

4. JVM类加载

  JVM启动时,会通过不同的类加载器加载不同的类

  JVM类加载采用 类加载器的双亲委派模型(Parents Dlegation Mode),如下图所示:

   

 

  JVM中包括集中类加载器:

  1 BootStrapClassLoader 引导类加载器

  2 ExtClassLoader 扩展类加载器

  3 AppClassLoader 应用类加载器

  4 CustomClassLoader 用户自定义类加载器

  

  需要注意的是,不同的类加载器加载的类是不同的,因此如果用户加载器1加载的某个类,其他用户并不能够使用。对于任意一个类,都需要由加载他的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。

  

  双亲委任模型时如何实现的?

  所有的代码都在java.lang.ClassLoader中的loadClass方法之中

  

5. 破坏双亲委任模型

到目前为止,双亲委派模型有过3次大规模的“被破坏”的情况。
  第一次:在双亲委派模型出现之前—–即JDK1.2发布之前。
  第二次:线程上下文类加载器(Thread Context ClassLoader)。

  我们说,双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API, 但没有绝对,如果基础类调用会用户的代码怎么办呢?为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader方法进行设置。如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认即是应用程序类加载器。

  在Java核心类里面有SPI(Service Provider Interface),它由Sun编写规范,第三方来负责实现。SPI需要用到第三方实现类。如果使用双亲委派模型,那么第三方实现类也需要放在Java核心类里面才可以,不然的话第三方实现类将不能被加载使用。但是这显然是不合理的!怎么办呢?ContextClassLoader(上下文类加载器)就来解围了。

  在java.lang.Thread里面有两个方法,get/set上下文类加载器

  1. public void setContextClassLoader(ClassLoader cl)
  2. public ClassLoader getContextClassLoader()

  我们可以通过在SPI类里面调用getContextClassLoader来获取第三方实现类的类加载器。由第三方实现类通过调用setContextClassLoader来传入自己实现的类加载器。这样就变相地解决了双亲委派模式遇到的问题。但是很显然,这种机制破坏了双亲委派模式

  一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时就放进去的rt.jar),但它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识“这些代码啊。因为这些类不在rt.jar中,但是启动类加载器又需要加载。怎么办呢?有了线程上下文加载器,JNDI服务使用这个线程上下文加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。但这无可奈何,Java中所有涉及SPI的加载动作基本胜都采用这种方式。例如JNDI,JDBC,JCE,JAXB,JBI等。

   第三次:为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。

  ps:Java 程序中基本有一个共识:OSGI对类加载器的使用时值得学习的,弄懂了OSGI的实现,就可以算是掌握了类加载器的精髓。

6. Tomcat类加载

  在tomcat中类的加载稍有不同

  TOMCAT6之前

  

  

  前面3个类加载和默认的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/*、/server/*、/shared/*(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。

  commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

  CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

 

  TOMCAT6之后

   

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

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

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

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

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

 

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

  1 使用bootstrap引导类加载器加载(JVM JRE)

  2 使用system系统类加载器加载CATALINA_HOME/bin

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

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

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

    Jar包的加载顺序是:

  1)JRE中的Java基础包
  2)Web应用WEB-INF\lib下的包
  3)Tomcat\lib下的包
  

  Tomcat基本遵守了JVM的委派模型,但也在自定义的类加载器中做了细微的调整,以适应Tomcat自身的要求。下面是Tomcat类加载器WebappClassLoader的核心方法loadClass()的源码。它覆盖了父类URLClassLoader中的方法,改变了默认的类加载顺序。首先findLoadedClass0()和findLoadedClass()分别从本地和父类加载器的缓存中查找当前要加载的类是否已经加载过了。 

public synchronized Class loadClass(String name, boolean resolve)throws ClassNotFoundException {
        Class clazz null;
        // (0) Check our previously loaded local class cache
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }
        // (0.1) Check our previously loaded class cache
        clazz = findLoadedClass(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }
        // (0.2) Try loading the class with the system class loader, to prevent
        //       the webapp from overriding J2SE classes
        try {
            clazz = system.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
        boolean delegateLoad = delegate || filter(name);
        // (1) Delegate to our parent if requested
        if (delegateLoad) {
            if (log.isDebugEnabled())
                log.debug("  Delegating to parent classloader1 " + parent);
            ClassLoader loader = parent;
            if (loader == null)
                loader = system;
            try {
                clazz = loader.loadClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from parent");
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                ;
            }
        }

        // (2) Search local repositories
        if (log.isDebugEnabled())
            log.debug("  Searching local repositories");
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from local repository");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            ;
        }


        // (3) Delegate to parent unconditionally
        if (!delegateLoad) {
            if (log.isDebugEnabled())
                log.debug("  Delegating to parent classloader at end: " + parent);
            ClassLoader loader = parent;
            if (loader == null)
                loader = system;
            try {
                clazz = loader.loadClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from parent");
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                ;
            }
        }
        throw new ClassNotFoundException(name);
    }

  

7. Tomcat启动流程

  tomcat的启动流程很标准化,入口是BootStrap,统一按照生命周期管理接口Lifecycle的定义进行启动。首先,调用init()方法逐级初始化,接着调用start()方法进行启动,同时,每次调用伴随着生命周期状态变更事件的触发。每一级组件除完成自身的处理外,还有负责调用子组件的相关调用,组件和组件之间是松耦合的,可以通过配置进行修改。大致流程图如下:

   

 

8. 问题扩展  

  1. 为什么java文件放在Eclipse中的src文件夹下会优先jar包中的class?

  这是因为Eclipse中的src文件夹中的文件java以及webContent中的JSP都会在tomcat启动时,被编译成class文件放在 WEB-INF/class 中。而Eclipse外部引用的jar包,则相当于放在 WEB-INF/lib 中。因此肯定是 java文件或者JSP文件编译出的class优先加载。

  2. 在 CATALINA_HOME/lib 以及 WEB-INF/lib 中放置了 不同版本的jar包,此时就会导致某些情况下报加载不到类的错误。

  3. 如果多个应用使用同一jar包文件,当放置了多份,就可能导致 多个应用间出现类加载不到的错误。

  4. 如果tomcat 的 Common ClassLoader 想加载 WebApp ClassLoader 中的类,该怎么办?  使用线程上下文类加载器实现。

  5. 为什么要使用双亲委派模式?

   如果没有使用双亲委派模型,由各个类加载器自行加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统将会出现多个不同的Object类, Java类型体系中最基础的行为就无法保证。应用程序也将会变得一片混乱。

  6. 为什么要违反双亲委派莫斯?

    基础类调用会用户的代码的情况,在Java核心类里面有SPI(Service Provider Interface),它由Sun编写规范,第三方来负责实现。SPI需要用到第三方实现类。

抄录地址

  1. 深入理解Tomcat(五)类加载机制
  2. Tomcat类加载机制和JAVA类加载机制的比较
  3. 面试专题(五):类加载机制和双亲委派模型
  4. 深入理解 Tomcat(四)Tomcat 类加载器之为何违背双亲委派模型
  5. Tomcat 类加载机制
  6. 图解Tomcat类加载机制(阿里面试题)
  7. tomcat-架构原理
posted @ 2019-04-21 17:48  海米傻傻  阅读(629)  评论(0编辑  收藏  举报