java标准类加载器
Bootstrap:用于加载JVM提供的基础运行类,即位于%JAVA_HOME%/jre/lib目录下的核心类库。
Extension:加载除核心类库外的Jar包,即只要复制到指定的扩展目录下的Jar,默认的扩展目录是%JAVA_HOME%/jre/lib/ext。
System:用于加载环境变量CLASSPATH指定目录下的或者-classpath运行参数指定的Jar包。System类加载器通常用于加载应用程序Jar包及其启动入口类(Tomcat的Bootstrap类即由System类加载器加载)。
Tomcat加载器
1、隔离性:Web应用类库相互隔离,避免依赖库或者应用包相互影响。如果两个Web项目,一个采用了Spring2.5,一个采用了Spring4.0,而应用服务器使用一个类加载器加载,那么Web应用将会由Jar包覆盖导致无法启动。
2、灵活性:如果重新部署Web项目,采用一个类加载器,类之间的依赖是杂乱无章的,无法完整的移除某个Web应用的类。
3、性能:由于灭个Web应用都有一个类加载器,因此Web应用在加载类时,不会搜索其他Web应用包含的Jar包,性能自然高于应用服务器只有一个类加载器的情况。
Common加载器: 以System为父类加载器,是位于Tomcat应用服务器顶层的公用类加载器。其路径为common.loader,默认指向$CATALINA_HOME/lib下的包。
Catalina:以Common为父加载器,是用于加载Tomcat应用服务器的类加载器,其路径为server.loader,默认为空。此时Tomcat使用Common类加载器加载应用服务器。
Shard:以Common为父加载器,是所有Web应用的父加载器,其路径为shared.loader,默认为空,此时Tomcat使用Common类加载器作为Web应用的父加载器。
Web应用:以Shared为父加载器,加载/WEB-INF/classes目录下的未压缩的class和资源文件以及/WEB-INF/lib目录下的Jar包。该加载器只对当前Web应用可见。
Web应用类加载器
我们都知道Java默认的类加载机制是委派模式,过程如下:
(1)从缓存中加载
(2)如果缓存中没有,则从父加载器中加载
(3)如果父类加载器中没有,则从当前类加载器加载
(4)如果没有,则抛异常。
Tomcat提供的Web应用类加载器与默认的委派模式稍有不同,当进行类加载时,除JVM基础类库外,会首先尝试通过当前类加载器加载,然后才进行委派。
Web应用类加载器默认加载顺序如下:
(1)从缓存中加载。
(2)如果没有则从JVM的Bootstrap类加载器加载。
(3)如果没有则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序)。
(4)如果没有则从父类加载器加载,由于父类加载器采用默认的委派模式,所以加载顺序为System、Common、Shared。
Tomcat提供了delegate属性用于控制是否启用Java委派模式,默认为false(不启用)。当配置为true时,则使用委派模式:
(1)从缓存中加载。
(2)如果没有则从JVM的Bootstrap类加载器加载。
(3)如果没有则从父类加载器加载(System、Common、Shared)。
(4)如果没有则从当前类加载器加载。
tomcat的设计已经打破了双亲委派模型,双亲委派模型并不是一个强制性的约束模型,在Java的世界中大部分类加载器都遵循这个模型,但也有其他的例外:
第一次打破双亲委派模型是发生在双亲委派模型出现之前(JDK1.2),因为双亲委派是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0的时代已经存在,用户继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()方法。面对已经存在的用户自定义类加载的实现代码,引入双亲委派模型时需要向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),并且不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写在findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法完成加载,这样就可以保证新写的类加载器时符合双亲委派规则的。
第二次被打破是因为这个模型自身的缺陷导致的,一个典型的例子就是JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的时对资源进行集中管理和查找,它需要调用由独立的厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI)的代码,但是这些代码启动类加载器不认识,为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序全局范围内都没有设置的话,那这个类加载器默认就是应用程序类加载器,JNDI服务使用这个线程上下文加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作。
第三次被打破是因为对程序动态性能的追求导致的,比如代码热替换,不需要重启服务器。例如OSGi,它实现模块化热部署的关键是自定义的类加载器机制实现的。每一个程序员模块都有一个自己的类加载器,当需要替换一个模块时,就把模块连同类加载器一起换掉以实现代码的热替换。
在OSGi环境下,类加载器不再时双亲委派模型,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:
(1)将以.java开头的类委派给父类加载器。
(2)否则将委派列表名单内的类委派给父类加载器。
(3)否则将Import列表中的类委派给Export这个类的模块的类加载器加载。
(4)否则查找当前模块的ClassPath,使用自己的类加载器。
(5)否则查找类是否再自己的Fragment模块中,如果在,则委派给其类加载器加载。
(6)否则查找Dynamic Import列表的模块,委派给对应模块的类加载器加载。
(7)否则加载失败。
上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类加载都是在平级的类加载器中进行的。