Apusic中的类装载(classloader)机制

本文转自Apusic官方文档,详细内容请访问http://infocenter.apusic.com 。

 

配置Classloader

JavaEE 规范定义了一个打包机制的框架,用来把JavaEE应用的各个部分组织在一起。不同的应用服务器厂商可以自由的设计自己的类装载层次来装载应用中的类和资 源。因此开发者必须非常清楚类和资源应该放置在什么位置对于JavaEE应用才是可用的。理解Apusic应用服务器的类装载体系结构能够帮助 JavaEE应用的开发者设计高效和可移植应用打包结构。本章先介绍类装载的基本概念,然后讨论了Apusic应用服务器的类装载层次是如何设计的。

Classloader的基本概念

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

  • 检查这个类有没有被装载过,如果已经装载过,则直接返回;

  • 调用父Classloader去装载类,如果装载成功直接返回;

  • 调用自身的装载类的方法,如果装载成功直接返回;

  • 上述所有步骤都没有成功装载到类,抛出ClassNotFoundException;

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

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

类装载的方式

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

  • 显式类装载

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

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

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

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

  • 隐式类装载

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

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

  • 类装载发生的时间

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

一个基本的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)。

基本的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驱动程序等。

自定义Classloader

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

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);
    …
    
JVM中类的唯一性

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

Apusic的Classloader体系

在上一节,我们了解了基本的Classloader层次结构模型,知道了Bootstrap Classloader、System Classloader的职责,还知道可以通过自定义Classloader来完成特定的装载任务,除此之外,我们还了解了什么是Caller Classloader和线程上下文Classloader。下面,我们就可以根据这些基本的Classloader概念去看看Apusic Classloader体系是长什么样的了。

JavaEE应用对Classloader的要求

Apusic 应用服务器本身运行需要的类都在CLASSPATH中,由System Classloader加载。在上一节中,我们提到Apusic应用服务器中定义了若干个专有的Classloader,负责装载部署在Apusic中的 JavaEE应用中的类和资源。Apusic为何要额外的去自定义Classloader呢?把应用需要的类和资源都放在CLASSPATH中, System Classloader不也可以加载这些类吗?要回答这些问题,我们先考虑一下下面两个简单的需求:

  • 不同的应用中,可能有同名的资源文件或类,它们在各自应用中有不同的行为或语义。

  • 应用发生变化的时候,例如改了Jsp或者JavaBean,在不重启服务器甚至不重启应用的情况下,需要立即看到修改的效果。

我们前面提到过在一个JVM中一个类的唯一标识,当不能改变类的包名和类名的情况下,除非 Classloader的实例发生变化,才有可能实现对一个类的再次加载。显然,在只有System Classloader的情况下,无法满足上面两个简单的需求。这是因为在运行期,我们无法重新创建System Classloader的实例,也没办法让它装载一个已经装载过的类

对 于第一个需求,我们可以对不同的应用中的类和资源进行隔离加载,这就需要为每个应用使用不同的Classloader实例;对于第二个需求,当Jsp或 JavaBean发生变化时,我们需要把原来装载Jsp的Classloader销毁掉,创建一个新的Classloader实例,并让它去装载修改后的 类,因此,要专门定义一个Classloader去负责装载Jsp、JavaBean,使得在重新创建Classloader时,受影响的范围尽可能的 小。

Apusic的Classloader和它们的层次结构

Apusic为装载JavaEE应用中的类定义了EJBClassLoader和ServletClassLoader这两个主要的Classloader。假设一个JavaEE应用的结构如下:

  • EJBClassLoader

    每 个JavaEE应用都有一个EJBClassLoader,用于装载EJB module和公共类。上图中的ejbjarA.jar、ejbjarB.jar、util.jar以及app.ear我们可以看成是一个jar文件,也 可以看成是一个目录,它们里边的类和文件都由同一个EJBClassLoader实例装载,因此,同一个JavaEE应用中的EJB module和公共类是相互可见的。

    不同的应用,其EJBClassLoader实例也不同,且每个EJBClassLoader实例间是平级关系,所以不同应用中的类是相互不可见的。

  • ServletClassLoader

    在Apusic应用服务器中,每个Web module都有一个ServletClassLoader,用于装载Web module中的类和资源文件。所以,每个JavaEE应用中都可能有一个或多个ServletClassLoader,例如上图表示的JavaEE应用就有两个ServletClassLoader,它们是平级关系,所以Web module中的类相互不可见。对于ServletClassLoader,还有一些特殊的行为,将在下一节介绍。

  • 层次结构

    通过以上的介绍,我们可以知道,Apusic应用服务器启动后,假设其中部署了两个应用,分别是appA.ear和appB.ear,那么其Classloader层次结构可表现为:

    其 中,我们可以看到,EJBClassLoader是ServletClassLoader的父,由ejbClassLoaderA装载的类和文件,对于 servletClassLoaderA和servletClassLoaderB装载的类都是可见的。也就是说,同一个应用中的任意Web Module的类(即位于WEB-INF/classes、WEB-INF/lib中的类),都可以使用ejb jar或util jar中的类。

    但对于上图中ejbClassLoaderA装载的类,servletClassLoaderC是看不见的,它们属于不同的应用。

ServletClassLoader的特性

在 前面几节,我们提到过Apusic对于Web module中的类,包括jsp(最终被应用服务器解析成servlet并编译成Java类)、WEB-INF/classes和WEB-INF/lib 里边的class和资源文件,专门定义一个ServletClassLoader进行加载是了满足类似开发期中类的动态加载、不同Module间类的隔离 等的需要。Apusic应用服务器在Classloader体系中做了充分的考虑以降低Web应用开发的复杂性及提升应用服务器的易用性。下面将介绍 Apusic的ServletClassLoader的行为特性:

类的动态加载

在Apusic检测到jsp或WEB-INF/classes目录下的类的更新后,会重新加载修改过的类。对于用户来说,不需要做任何事情,在修改完后马上调用该类就可以看到刚刚做的更新。

考 虑到运行期和开发期的要求不同,运行期类和资源文件不会频繁更新,因此,在运行期,不需要频繁检测类文件是否已经更新,可通过配置apusic.conf 中的ServletReloadCheckInterval属性值来修改检测时间。当值小于”0”时,不检测。此值默认是3,即每3秒中检测一次。

ServletClassLoader的多层结构

ServletClassLoader是一层壳,根据配置的不同策略,委托给不同的Classloader执行装载任务。Servlet Classloader的装载行为有两种策略,可通过配置进行指定,配置有两种方式:

  • 在web.xml中增加Context Parameter

    <context-param>
        <param-name>com.apusic.web.ServletClassLoaderDelegate</param-name>
        <param-value>Separated</param-value>
    </context-param>

    这样的配置有效范围只有当前应用。如果修改的是$DOMAIN_HOME/config/web.xml下的配置,则适用所有应用。

  • 通过VM参数指定

    -Dcom.apusic.web.ServletClassLoaderDelegate=Separated

    这种系统属性配置,所有的应用都生效。

ServletClassLoader的两种装载策略分别通过com.apusic.web.ServletClassLoaderDelegate的两个值来指定:

  • Composite

    默 认值,表示ServletClassLoader的行为委托给了两层Classloader,一层叫CompositeClassLoader,它的父 Classloader是EJBClassLoader,它负责WEB-INF/lib和WEB-INF/classes目录下的类和资源的装载,其中, 如果在WEB-INF/lib和WEB-INF/classes下有同名的类或资源,WEB-INF/classes下的类将被优先装载;另一层是 JSPClassLoader,它的父Classloader是CompositeClassLoader,它负责装载解析编译后的JSP。

  • Separated

    表 示ServletClassLoader的行为委托给了三层Classloader,跟上一种策略不同的是WEB-INF/lib下类和WEB- INF/classes目录下的类和资源由不同的Classloader装载,前者叫StaticClassLoader,它的父是 EJBClassLoader;后者我们称为ReloadableClassLoader,它的父是StaticClassLoader,子是 JSPClassLoader。根据前面对Classloader父子关系的描述,我们可以知道,WEB-INF/lib下的类看不见WEB- INF/classes下的类,而WEB-INF/classes下的类可以看见WEB-INF/lib下的类。考虑到客户应用中,资源文件一般放在 WEB-INF/classes目录中,因此,如果在WEB-INF/lib和WEB-INF/classes下有同名的资源文件,仍然是WEB- INF/classes下的资源优先装载。

如果客户应用系统中,WEB-INF/lib下的类会引用WEB-INF/classes下的类或资源,或者认为WEB-INF/classes下的类应该优先于WEB-INF/lib下的类装载,我们建议使用Composite,即默认的策略。

如 果考虑到在开发期WEB-INF/lib下的类或文件不会频繁更新,为了避免检测范围太大而导致的检测时间过长,不扫描WEB-INF/lib下的更新 (即此目录下的类只被装载一次,如果有更新,则需要重启应用才能生效),或者认为WEB-INF/lib下的类应该优先于WEB-INF/classes 下的类装载时,可采用Separated策略。

Session中对象的类动态装载

如果session中保存的对象实例的类发生了更改,且类的签名未发生变化,那么对象实例的类型信息将被标识为新装载的类,从Session中取出对象后,它的行为按更新后的类执行。但如果类的签名发生了变化,那么此session中的对象实例将被丢弃。

类装载的Web优先策略

在默认情况下,ServletClassLoader遵循大多数Classloader的装载行为,如“Classloader的基本概念 ” 一节描述的那样,会按顺序向上询问其所有父节点装载,如果父没装载到,才会由自身进行加载。这种默认的Java类装载机制有时也会碰到麻烦,比如WEB- INF/classes中有某个类,在系统Classpath中有这个类的另一个版本,Classloader默认的装载行为决定了系统 Classpath中的类会被优先加载。如果我们期望WEB-INF/classes中的类要优先加载,Apusic的Servlet Classloader提供了机会,可以通过配置系统属性或者在web.xml中增加Context Parameter来达到此目的:

  • 在web.xml中增加Context Parameter

    <context-param>
        <param-name>apusic.prefer.war.classes</param-name>
        <param-value>true</param-value>
    </context-param>

    这样的配置有效范围只有当前应用。如果修改的是$DOMAIN_HOME/config/web.xml下的配置,则适用所有应用。

  • 通过VM参数指定

    -Dapusic.prefer.war.classes=true

    这种系统属性配置,所有的应用都生效。

类装载查看服务

Apusic应用服务器提供了类装载查看服务,通过此服务,可以查找指定的类是由哪一层的Classloader装载的,类文件路径等信息,从而可以协助排查一些跟类装载相关的问题。类装载查看服务的相关配置段如下:

...
<SERVICE
    CLASS="com.apusic.util.ClassLoaderViewer"
    >
</SERVICE>
...

可以通过Admin Console上提供的类加载器来访问类装载查看服务,如何使用类加载器请参考Admin Console文档。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值