类加载器的类别

启动类加载器(Bootstrap)

启动类加载器也叫根加载器,采用c++编写,用于加载Java的核心类,即JAVA_HOME/jre/lib下的jar包中的类:
image.png


拓展类加载器(Extension)

拓展类加载器采用Java编写,用于加载拓展类,即 JAVA_HOME/jre/lib/ext下的jar包中的类:
image.png

应用程序加载器(AppClassLoader)

应用程序加载器采用Java编写,也叫系统类加载器,用于加载应用程序ClassPath下的类(包含jar包中的类),程序员自己编写的类由该类加载器加载。

自定义加载器

程序员可以自定义类加载器,只需要继承 ClassLoader类和覆盖findClass()方法,不过AppClassLoader一般来说是完全够用的,一般不需要使用自定义的类加载器。

双亲委派机制

类加载器层次结构

类加载器 - 图3
其中根加载器采用c++编写,位于最高地层次,拓展类加载器在根加载器之下,应用程序加载器在拓展类加载器之下,最后就是用户自定义地类加载器。由于ExtClassLoader和AppClassLoader采用Java编写,并且它们是父子类关系,因此可以通过Java程序获取这两个类加载器,而根加载器由于采用c++编写而无法使用Java程序获取。

双亲委派机制原理

所有的类加载器都遵循双亲委派机制:
当任何层次的一个类加载器去加载类时先尝试让父类加载器去加载,如果父类加载器加载不了再尝试自身加载 。

比如:AppClassLoader收到加载某个类的请求,它会先把请求给它的父类ExtClassLoader,而ExtClassLoader又会给它的父类BootstrapClassLoader。因此,首先由根加载器加载这个类,如果BootstrapClassLoader无法加载,则才会让ExtClassLoader加载,如果ExtClassLoader还是无法加载,最后才会给到AppClassLoader去加载。
image.png
关于ExtClassLoader继承BootstrapClassLoader其实是不准确或者不正确的,因为BootstrapClassLoader是采用c++编写的,使用Java代码是无法获取BootstrapClassLoader的,如:
类加载器 - 图5
双亲委派模型能保证基础类仅加载一次,不会让jvm中存在重名的类。比如String.class,每次加载都委托给父加载器,最终都是BootstrapClassLoader,都保证java核心类都是BootstrapClassLoader加载的,保证了java的安全与稳定性。

注意: 双亲委派机制主要为了确保Java核心库的组件总是正确地被加载,避免重复加载

破坏双亲委派机制

双亲委派模型不是一个强制性的约束模型,双亲委派模型也有不太适用的时候,这时根据具体的情况我们就要破坏这种机制,下面介绍两种破坏双亲委派的情况:

线程上下文类加载器

线程上下文加载器就是Thread.currentThread().getContextClassLoader():从方法名字来看,应该是获取当前上下文的类加载器。

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径CLASSPATH里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派SystemClassLoader来加载类。

ClassLoader A -> System ClassLoader-> Extension Classloader -> Bootstrap Classloader

那么委派链左边的ClassLoader就可以很自然的使用右边的ClassLoader所加载的类。但如果情况要反过来,是右边的ClassLoader所加载的代码需要反过来去找委派链靠左边的ClassLoader去加载东西怎么办呢?没辙,双亲委派是单向的,没办法反过来从右边找左边。
于是,Thread就把当前的类加载器给保存下来了,其他加载器需要的时候,就通过当前线程的加载器获取到。每一个Thread都有一个相关联的Context ClassLoader(由native方法建立的除外),可以通过Thread.setContextClassLoader()方法设置。如果没有主动设置,Thread默认集成Parent Thread的 Context ClassLoader(注意是parent Thread 而不是父类)。如果我们的整个应用中都没有对此作任何处理,那么所有的Thread都会以System ClassLoader作为Context ClassLoader。知道这一点很重要,因为从web服务器,java企业服务器使用一些复杂而且精巧的ClassLoader结构去实现诸如JNDI、线程池和热部署等功能以来,这种简单的情况越发的少见了,一般都会使用特定的classloader来设置thread context classLoader。
image.png

自定义类加载器

破坏委派双亲模型就是由于用户追求动态性导致的,“动态性”就是指代码热替换、模块热部署等,就是希望程序不需要重启就可以更新class文件,最典型的例子就是SpringBoot的热部署和OSGi。这里拿OSGi举例,OSGi实现模块化热部署的关键就是它自定义类加载机制的实现,每一个程序模块(OSGi中称为Bundle)都有自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉实现热部署。所以,在OSGi环境下,类加载器不再是层次模型,而是网状模型。当OSGi收到一个类加载的时候会按照以下的顺序进行搜索:

  1. 将以 java.* 开头的类委派给父类加载器加载
  2. 否则,将委派列表名单内的类委派给父类加载器加载
  3. 否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载
  4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
  5. 检查Fragment Bundle中是否可以加载
  6. 查找Dynamic Import列表的Bundle
  7. 若以上都没有进行类加载,则加载失败

    Tomcat的类加载模式(了解)

    Web服务器存在的自身问题

    前面了解到了Java中类加载器的运行方式,但主流的Web服务器都会有自己的一套类加载器,因为对于服务器来说他要自己解决一些问题:

  8. 部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离。两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用。

  9. 部署在同一个Web容器上的两个Web应用程序所使用的相同的类库相同的版本可以互相共享。例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到Web容器的内存,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。
  10. Web容器需要尽可能地保证自身的安全不受部署的Web应用程序影响。Web容器也有用Java实现的,那么肯定不能把Web容器的类库和程序的类库弄混。
  11. 支持jsp的web容器,要支持热部署。我们知道运行jsp时实际上会先将jsp翻译成servlet,再编译为.class再在虚拟机运行起来再返回给客户端。而我们在编写jsp时,当tomcat服务器正在运行的时候,我们直接在jsp中修改代码时并不需要重启服务器,这就是达到了动态加载类的效果。

显然,如果Tomcat使用默认的类加载机制是无法满足上述要求的:

  1. 无法加载两个相同类库的不同版本的,因为默认类加载只在乎权限定类名,第一条不行。
  2. 可以实现。
  3. 默认类加载只在乎权限定类明,所以第三条不行。
  4. 前文我们说过,JVM确定是否为同一个类对象会要求类和类加载器都相同,默认的肯定不行,但我们可以想到当改变jsp代码的时候就改一次类加载器。

    Tomcat的类加载流程

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

目录结构大致为:

  • /common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
  • /server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见。
  • /shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
  • /WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

因此就解决了上面的四个问题:

  1. 部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离:各个WebAppClassLoader实例之间相互隔离
  2. 部署在同一个Web容器上的两个Web应用程序所使用的相同的类库相同的版本可以互相共享:可以放在Common或Shared目录下让这些程序共享
  3. Web容器需要尽可能地保证自身的安全不受部署的Web应用程序影响:CatalinaClassLoader加载web服务器需要的类库,WebAppClassLoader只能得到SharedClassLoader的类库
  4. 支持jsp的web容器,要支持热部署:每当改变jsp时,更新JasperClassLoader