24.1. Classloader的基本概念

Classloader在运行期会以父/子的层次结构存在,每个Classloader的实例都持有其父Classloader的引用,而父Classloader并不持有子Classloader的引用,从而形成一条单向链,当一个类装载请求被提交到某个Classloader时,其默认的类装载过程如下:

每一层次的Classloader都重复上述动作。

简单说,当Classloader链上的某一Classloader收到类装载请求时,会按顺序向上询问其所有父节点,直至最顶端(BootstrapClassLoader),任何一个节点成功受理了此请求,则返回,如果所有父节点都不能受理,这时候才由被请求的Classloader自身来装载这个类,如果仍然不能装载,则抛出异常。

24.1.1. 类装载的方式

类装载的方式主要有两种:显式的和隐式的。

  • 显式类装载

    发生在使用以下方法调用进行装载类的时候:

    • ClassLoader.loadClass()(使用指定的Classloader进行装载)

    • Class.forName()(使用当前类的Caller Classloader进行装载)

      当调用上述方法的时候,指定的Class(以类名为参数)由Classloader装入。这两个方法的行为有轻微的区别,Class.forName()在类装载完成后,会对类进行初始化,而ClassLoader.loadClass()只负责装载类。

  • 隐式类装载

    发生在由于引用、实例化或继承导致需要装载类的时候。隐式类装载是在幕后启动的,JVM会解析必要的引用并装载类。

    类的装载通常组合了显式和隐式两种方式。例如,Classloader可能先显式地装载一个类,然后再隐式地装载它引用的其它类。

  • 类装载发生的时间

    从类装载方式的描述中我们可以看到,只有在显式的调用方法或者实例化、引用、继承一个类时,类才真正被装载。由此,我们可以知道,import并不会导致类装载,以及,在一个类实例化之前,调用它的静态方法,会导致这个类和它的父类、实现的接口和相关的静态成员的类会被装载,而它的成员变量的类却不会被装载

24.1.2. 一个基本的Classloader的层次结构

上图显示了一个基本的Classloader的层次结构。在给定层次上的Classloader不能引用任何层次低于它的Classloader,另外,它的子Classloader装载的类对于其是不可见的。在上图中,如果Foo.class是由ClassLoaderB装载的,并且Foo.class依赖于Bar.class,那么Bar.class必须由ClassLoaderA或B装载。如果Bar.class只是对ClassLoaderC和D可见,那么将会发生ClassNotFoundException或者NoClassDefFoundError异常。

如果Bar.class分别对于两个平级的Classloader可见(例如C和D),但对于它们的父Classloader不可见,那么当类装载请求发送到这两个Classloader时,每一个Classloader会装载自己版本的类。ClassLoaderC装载的Bar.class的实例将不兼容于ClassLoaderD装载的Bar.class的实例。如果对Classloader的层次结构不了解,试图使用由ClassLoaderC装载的类去造型一个ClassLoaderD装载的Bar.class的实例,则会发生造型失败(ClassCastException)。

24.1.3. 基本的Classloader

最基本的Classloader是Bootstrap Classloader和System Classloader(也有人称之为AppClassLoader),只要写过java程序,都会用到这两个Classloader。

  • Bootstrap Classloader

    这个Classloader装载Java虚拟机提供的基本运行时刻类($JAVA_HOME/jre/lib),还包括放置在系统扩展目录($JAVA_HOME/jre/lib/ext)内的JAR文件中的类。这个Classloader是java程序最顶层的Classloader,只有它没有父Classloader。如果你将一个自己写的类或第三方jar包放进$JAVA_HOME/jre/lib/ext目录中,那么它将被Bootstrap Classloader装载。

  • System Classloader

    System Classloader通常负责装载系统环境变量CLASSPATH中设置的类。由System Classloader装载的类对于Apusic服务器内部的类和部署在Apusic服务器上的J2EE应用(通常打包成ear)都是可见的。%APUSIC_HOME%/lib目录下的jar文件是Apusic应用服务器的核心类,一般把这些jar文件都加在系统CLASSPATH中。另外,一些公用类也可以加在系统CLASSPATH中,如JDBC驱动程序等。

24.1.4. 自定义Classloader

在编写应用代码的时候,常常有需要动态加载类和资源,比如显式的调用classLoader.loadClass(“ClassName”),虽然直接使用ClassLoader.getSystemClassLoader(),可以得到SystemlassLoader来完成这项任务。但是,由于System Classloader是JVM创建的Classloader,它的职责有限,只适合于普通的java应用程序,在很多复杂场景中不能满足需求,比如在应用服务器中。这时候就需要自行实现一个Classloader的子类,实现特定的行为。Apusic应用服务器中就定义了若干个特有的Classloader,负责装载部署在Apusic中的JavaEE应用中的类,这里并不试图去描述如何实现一个自定义的Classloader,但本章第二部分将详细描述Apusic自定义的Classloader的行为。

24.1.5. Caller Classloader和线程上下文Classloader

动态加载资源时,往往有三种Classloader可选择:System Classloader、Caller Classloader、当前线程的上下文Classloader。System Classloader前面已经描述过了,下面我们看看什么是Caller Classloader、当前线程的上下文Classloader。

  • Caller Classloader

    Caller Classloader指的是当前所在的类装载时使用的Classloader,它可能是System Classloader,也可能是一个自定义的Classloader,这里,我们都称之为Caller Classloader。我们可以通过getClass().getClassLoader()来得到Caller Classloader。例如,存在A类,是被AClassLoader所加载,A.class.getClassLoader()为AClassLoader的实例,它就是A.class的Caller Classloader。

    如果在A类中使用new关键字,或者Class.forName(String className)和Class.getResource(String resourceName)方法,那么这时也是使用Caller Classloader来装载类和资源。比如在A类中初始化B类:

    /**
      * A.java
    */
    ...
    public void foo() {
        B b = new B();
        b.setName("b");
    }
    

    那么,B类由当前Classloader,也就是AClassloader装载。同样的,修改上述的foo方法,其实现改为:

    Class clazz = Class.forName("foo.B");

    最终获取到的clazz,也是由AClassLoader所装载。

    那么,如何使用指定的Classloader去完成类和资源的装载呢?或者说,当需要去实例化一个Caller Classloader和它的父Classloader都不能装载的类时,怎么办呢?

    一个很典型的例子是JAXP,当使用xerces的SAX实现时,我们首先需要通过rt.jar中的javax.xml.parsers.SAXParserFactory.getInstance()得到xercesImpl.jar中的org.apache.xerces.jaxp.SAXParserFactoryImpl的实例。由于JAXP的框架接口的class位于JAVA_HOME/lib/rt.jar中,由Bootstrap Classloader装载,处于Classloader层次结构中的最顶层,而xercesImpl.jar由低层的Classloader装载,也就是说SAXParserFactoryImpl是在SAXParserFactory中实例化的,如前所述,使用SAXParserFactory的Caller Classloader(这里是Bootstrap Classloader)是完成不了这个任务的。

    这时,我们就需要了解一下线程上下文Classloader了。

  • 线程上下文Classloader

    每个线程都有一个关联的上下文Classloader。如果使用new Thread()方式生成新的线程,新线程将继承其父线程的上下文Classloader。如果程序对线程上下文Classloader没有任何改动的话,程序中所有的线程将都使用System Classloader作为上下文Classloader。

    当使用Thread.currentThread().setContextClassLoader(classloader)时,线程上下文Classloader就变成了指定的Classloader了。此时,在本线程的任意一处地方,调用Thread.currentThread().getContextClassLoader(),都可以得到前面设置的Classloader。

    回到JAXP的例子,假设xercesImpl.jar只有AClassLoader能装载,现在A.class内部要使用JAXP,但是A.class却不是由AClassLoader或者它的子Classloader装载的,那么在A.class中,应该这样写才能正确得到xercesImpl的实现:

    AClassLoader aClassLoader = new AClassLoader(parent);
    Thread.currentThread().setContextClassLoader(aClassLoader);
    SAXParserFactory factory = SAXParserFactory.getInstance();
    ...
    

    JAXP这时就可以通过线程上下文Classloader装载xercesImpl的实现类了,当然,还有一个前提是在配制文件或启动参数中指定了使用xerces作为JAXP的实现。下面是JAXP中的代码片断:

    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    …
    Class providerClass = cl.loadClass(className);
    …
    

24.1.6. JVM中类的唯一性

JVM为每一个Classloader维护一个唯一标识。在一个JVM里(对应一个Java进程),可以由不同的Classloader装载多个同名的类(指包名和类名都完全相同,下同),为了唯一地标识被不同Classloader装载的类,JVM会在被装载的类名前加上装载该类的Classloader的标识。