java ClassLoader(转)

来源:互联网 发布:贵州大数据交易中心 编辑:程序博客网 时间:2024/05/12 05:41

Java ClassLoader


当JVM(Java虚拟机)启动时,会形成由三个类加载器组成的初始类加载器层次结构:

      bootstrap classloader
              |
      extension classloader
              |
      system classloader

bootstrap classloader-引导(也称为原始)类加载器,它负责加载Java的核心类。在Sun的JVM中,在执行java的命令中使用-Xbootclasspath选项或使用- D选项指定sun.boot.class.path系统属性值可以指定附加的类。这个加载器的是非常特殊的,它实际上不是java.lang.ClassLoader的子类,而是由JVM自身实现的。大家可以通过执行以下代码来获得bootstrapclassloader加载了那些核心类库:
   URL[]urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
   for (int i = 0; i< urls.length; i++) {
    System.out.println(urls.toExternalform());
   }
在我的计算机上的结果为:
文件:/C:/j2sdk1.4.1_01/jre/lib/endorsed/dom.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/endorsed/sax.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xalan-2.3.1.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xercesImpl-2.0.0.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xml-apis.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xsltc.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/rt.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/i18n.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/sunrsasign.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/jsse.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/jce.jar
文件:/C:/j2sdk1.4.1_01/jre/lib/charsets.jar
文件:/C:/j2sdk1.4.1_01/jre/classes
这时大家知道了为什么我们不需要在系统属性CLASSPATH中指定这些类库了吧,因为JVM在启动的时候就自动加载它们了。

extension classloader-扩展类加载器,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的)中JAR的类包。这为引入除Java核心类以外的新功能提供了一个标准机制。因为默认的扩展目录对所有从同一个JRE中启动的JVM都是通用的,所以放入这个目录的JAR类包对所有的JVM和systemclassloader都是可见的。在这个实例上调用方法getParent()总是返回空值null,因为引导加载器bootstrapclassloader不是一个真正的ClassLoader实例。所以当大家执行以下代码时:
  System.out.println(System.getProperty("java.ext.dirs"));
   ClassLoaderextensionClassloader=ClassLoader.getSystemClassLoader().getParent();
   System.out.println("the parentof extension classloader :"+extensionClassloader.getParent());
结果为:
C:\j2sdk1.4.1_01\jre\lib\ext
the parent of extension classloader : null
extension classloader是system classloader的parent,而bootstrapclassloader是extensionclassloader的parent,但它不是一个实际的classloader,所以为null。

system classloader-系统(也称为应用)类加载器,它负责在JVM被启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径。总能通过静态方法ClassLoader.getSystemClassLoader()找到该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器。执行以下代码即可获得:
  System.out.println(System.getProperty("java.class.path"));
输出结果则为用户在系统属性里面设置的CLASSPATH。
classloader加载类用的是全盘负责委托机制。所谓全盘负责,即是当一个classloader加载一个Class的时候,这个Class所依赖的和引用的所有Class也由这个classloader负责载入,除非是显式的使用另外一个classloader载入;委托机制则是先让parent(父)类加载器(而不是super,它与parentclassloader类不是继承关系)寻找,只有在parent找不到的时候才从自己的类路径中去寻找。此外类加载还采用了cache机制,也就是如果cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是为什么我们修改了Class但是必须重新启动JVM才能生效的原因。


每个ClassLoader加载Class的过程是:
1.检测此Class是否载入过(即在cache中是否有此Class),如果有到8,如果没有到2
2.如果parent classloader不存在(没有parent,那parent一定是bootstrapclassloader了),到4
3.请求parent classloader载入,如果成功到8,不成功到5
4.请求jvm从bootstrap classloader中载入,如果成功到8
5.寻找Class文件(从与此classloader相关的类路径中寻找)。如果找不到则到7.
6.从文件中载入Class,到8.
7.抛出ClassNotFoundException.
8.返回Class.

其中5.6步我们可以通过覆盖ClassLoader的findClass方法来实现自己的载入策略。甚至覆盖loadClass方法来实现自己的载入过程。

类加载器的顺序是:
先 是bootstrap classloader,然后是extension classloader,最后才是systemclassloader。大家会发现加载的Class越是重要的越在靠前面。这样做的原因是出于安全性的考虑,试想如果systemclassloader“亲自”加载了一个具有破坏性的“java.lang.System”类的后果吧。这种委托机制保证了用户即使具有一个这样的类,也把它加入到了类路径中,但是它永远不会被载入,因为这个类总是由bootstrapclassloader来加载的。大家可以执行一下以下的代码:
  System.out.println(System.class.getClassLoader());
将会看到结果是null,这就表明java.lang.System是由bootstrapclassloader加载的,因为bootstrapclassloader不是一个真正的ClassLoader实例,而是由JVM实现的,正如前面已经说过的。

下面就让我们来看看JVM是如何来为我们来建立类加载器的结构的:
sun.misc.Launcher,顾名思义,当你执行java命令的时候,JVM会先使用bootstrapclassloader载入并初始化一个Launcher,执行下来代码:
  System.out.println("the Launcher's classloaderis"+sun.misc.Launcher.getLauncher().getClass().getClassLoader());
结果为:
  the Launcher's classloader is null(因为是用bootstrap classloader加载,所以class loader为null)
Launcher 会根据系统和命令设定初始化好class loader结构,JVM就用它来获得extensionclassloader和systemclassloader,并载入所有的需要载入的Class,最后执行java命令指定的带有静态的main方法的Class。extensionclassloader实际上是sun.misc.Launcher$ExtClassLoader类的一个实例,systemclassloader实际上是sun.misc.Launcher$AppClassLoader类的一个实例。并且都是java.net.URLClassLoader的子类。

让我们来看看Launcher初试化的过程的部分代码。

Launcher的部分代码:
public class Launcher  {
   public Launcher() {
      ExtClassLoader extclassloader;
      try {
         //初始化extension classloader
         extclassloader =ExtClassLoader.getExtClassLoader();
      } catch(IOException ioexception) {
          throw newInternalError("Could not create extension class loader");
      }
      try {
          //初始化systemclassloader,parent是extension classloader
          loader =AppClassLoader.getAppClassLoader(extclassloader);
      } catch(IOException ioexception1) {
          throw newInternalError("Could not create application class loader");
      }
      //将system classloader设置成当前线程的contextclassloader(将在后面加以介绍)
      Thread.currentThread().setContextClassLoader(loader);
      ......
   }
   public ClassLoadergetClassLoader() {
      //返回system classloader
      return loader;
   }
}

extension classloader的部分代码:
static class Launcher$ExtClassLoader extends URLClassLoader {

   public staticLauncher$ExtClassLoader getExtClassLoader()
      throws IOException
   {
      File afile[] = getExtDirs();
      return(Launcher$ExtClassLoader)AccessController.doPrivileged(newLauncher$1(afile));
   }
  private static File[] getExtDirs() {
      //获得系统属性“java.ext.dirs”
      String s =System.getProperty("java.ext.dirs");
      File afile[];
      if(s != null) {
         StringTokenizer stringtokenizer = newStringTokenizer(s, File.pathSeparator);
          int i =stringtokenizer.countTokens();
          afile = newFile;
          for(int j =0; j < i; j++)
             afile[j] = newFile(stringtokenizer.nextToken());

      } else {
          afile = newFile[0];
      }
      return afile;
   }
}

system classloader的部分代码:
static class Launcher$AppClassLoader extends URLClassLoader
{

   public static ClassLoadergetAppClassLoader(ClassLoader classloader)
      throws IOException
   {
      //获得系统属性“java.class.path”
      String s =System.getProperty("java.class.path");
      File afile[] = s != null ? Launcher.access$200(s): new File[0];
      return(Launcher$AppClassLoader)AccessController.doPrivileged(newLauncher$2(s, afile, classloader));
   }
}

看 了源代码大家就清楚了吧,extensionclassloader是使用系统属性“java.ext.dirs”设置类搜索路径的,并且没有parent。systemclassloader是使用系统属性“java.class.path”设置类搜索路径的,并且有一个parentclassloader。Launcher初始化extension classloader,systemclassloader,并将system classloader设置成为contextclassloader,但是仅仅返回system classloader给JVM。

  这里怎么又出来一个contextclassloader呢?它有什么用呢?我们在建立一个线程Thread的时候,可以为这个线程通过setContextClassLoader方法来指定一个合适的classloader作为这个线程的contextclassloader,当此线程运行的时候,我们可以通过getContextClassLoader方法来获得此contextclassloader,就可以用它来载入我们所需要的Class。默认的是systemclassloader。利用这个特性,我们可以“打破”classloader委托机制了,父classloader可以获得当前线程的contextclassloader,而这个contextclassloader可以是它的子classloader或者其他的classloader,那么父classloader就可以从其获得所需的Class,这就打破了只能向父classloader请求的限制了。这个机制可以满足当我们的classpath是在运行时才确定,并由定制的classloader加载的时候,由system classloader(即在jvmclasspath中)加载的class可以通过contextclassloader获得定制的classloader并加载入特定的class(通常是抽象类和接口,定制的classloader中是其实现),例如web应用中的servlet就是用这种机制加载的.


好了,现在我们了解了classloader的结构和工作原理,那么我们如何实现在运行时的动态载入和更新呢?只要我们能够动态改变类搜索路径和清除classloader的cache中已经载入的Class就行了,有两个方案,一是我们继承一个classloader,覆盖loadclass方法,动态的寻找Class文件并使用defineClass方法来;另一个则非常简单实用,只要重新使用一个新的类搜索路径来new一个classloader就行了,这样即更新了类搜索路径以便来载入新的Class,也重新生成了一个空白的cache(当然,类搜索路径不一定必须更改)。噢,太好了,我们几乎不用做什么工作,java.netURLClassLoader正是一个符合我们要求的classloader!我们可以直接使用或者继承它就可以了!

这是j2se1.4 API的doc中URLClassLoader的两个构造器的描述:
URLClassLoader(URL[] urls)
        Constructs a newURLClassLoader for the specified URLs using the default delegationparent ClassLoader.
URLClassLoader(URL[] urls, ClassLoader parent)
        Constructs a newURLClassLoader for the given URLs.
其中URL[] urls就是我们要设置的类搜索路径,parent就是这个classloader的parentclassloader,默认的是system classloader。


好,现在我们能够动态的载入Class了,这样我们就可以利用newInstance方法来获得一个Object。但我们如何将此Object造型呢?可以将此Object造型成它本身的Class吗?

首先让我们来分析一下java源文件的编译,运行吧!javac命令是调用“JAVA_HOME/lib/tools.jar”中的“com.sun.tools.javac.Main”的compile方法来编译:

   public static intcompile(String as[]);

   public static intcompile(String as[], PrintWriter printwriter);

返回0表示编译成功,字符串数组as则是我们用javac命令编译时的参数,以空格划分。例如:
javac -classpath c:\foo\bar.jar;. -d c:\ c:\Some.java
则字符串数组as为{"-classpath","c:\\foo\\bar.jar;.","-d","c:\\","c:\\Some.java"},如果带有PrintWriter参数,则会把编译信息出到这个指定的printWriter中。默认的输出是System.err。

其中 Main是由JVM使用Launcher初始化的systemclassloader载入的,根据全盘负责原则,编译器在解析这个java源文件时所发现的它所依赖和引用的所有Class也将由systemclassloader载入,如果system classloader不能载入某个Class时,编译器将抛出一个“cannotresolve symbol”错误。

所以首先编译就通不过,也就是编译器无法编译一个引用了不在CLASSPATH中的未知Class的java源文件,而由于拼写错误或者没有把所需类库放到CLASSPATH中,大家一定经常看到这个“cannotresolve symbol”这个编译错误吧!

其 次,就是我们把这个Class放到编译路径中,成功的进行了编译,然后在运行的时候不把它放入到CLASSPATH中而利用我们自己的classloader来动态载入这个Class,这时候也会出现“java.lang.NoClassDefFoundError”的违例,为什么呢?

我 们再来分析一下,首先调用这个造型语句的可执行的Class一定是由JVM使用Launcher初始化的systemclassloader载入的,根据全盘负责原则,当我们进行造型的时候,JVM也会使用systemclassloader来尝试载入这个Class来对实例进行造型,自然在systemclassloader寻找不到这个Class时就会抛出“java.lang.NoClassDefFoundError”的违例。

OK, 现在让我们来总结一下,java文件的编译和Class的载入执行,都是使用Launcher初始化的systemclassloader作为类载入器的,我们无法动态的改变systemclassloader,更无法让JVM使用我们自己的classloader来替换systemclassloader,根据全盘负责原则,就限制了编译和运行时,我们无法直接显式的使用一个systemclassloader寻找不到的Class,即我们只能使用Java核心类库,扩展类库和CLASSPATH中的类库中的Class。

还 不死心!再尝试一下这种情况,我们把这个Class也放入到CLASSPATH中,让systemclassloader能够识别和载入。然后我们通过自己的classloader来从指定的class文件中载入这个Class(不能够委托parent载入,因为这样会被systemclassloader从CLASSPATH中将其载入),然后实例化一个Object,并造型成这个Class,这样JVM也识别这个Class(因为systemclassloader能够定位和载入这个Class从CLASSPATH中),载入的也不是CLASSPATH中的这个Class,而是从CLASSPATH外动态载入的,这样总行了吧!十分不幸的是,这时会出现“java.lang.ClassCastException”违例。

为什么呢?我们也来分析一下,不错,我们虽然从CLASSPATH外使用我们自己的classloader动态载入了这个Class,但将它的实例造型的时候是JVM会使用systemclassloader来再次载入这个Class,并尝试将使用我们的自己的classloader载入的Class的一个实例造型为systemclassloader载入的这个Class(另外的一个)。大家发现什么问题了吗?也就是我们尝试将从一个classloader载入的Class的一个实例造型为另外一个classloader载入的Class,虽然这两个Class的名字一样,甚至是从同一个class文件中载入。但不幸的是JVM却认为这个两个Class是不同的,即JVM认为不同的classloader载入的相同的名字的Class(即使是从同一个class文件中载入的)是不同的!这样做的原因我想大概也是主要出于安全性考虑,这样就保证所有的核心Java类都是systemclassloader载入的,我们无法用自己的classloader载入的相同名字的Class的实例来替换它们的实例。

看到这里,聪明的读者一定想到了该如何动态载入我们的Class,实例化,造型并调用了吧!

那 就是利用面向对象的基本特性之一的多形性。我们把我们动态载入的Class的实例造型成它的一个systemclassloader所能识别的父类就行了!这是为什么呢?我们还是要再来分析一次。当我们用我们自己的classloader来动态载入这我们只要把这个Class的时候,发现它有一个父类Class,在载入它之前JVM先会载入这个父类Class,这个父类Class是systemclassloader所能识别的,根据委托机制,它将由systemclassloader载入,然后我们的classloader再载入这个Class,创建一个实例,造型为这个父类Class,注意了,造型成这个父类Class的时候(也就是上溯)是面向对象的java语言所允许的并且JVM也支持的,JVM就使用systemclassloader再次载入这个父类Class,然后将此实例造型为这个父类Class。大家可以从这个过程发现这个父类Class都是由system classloader载入的,也就是同一个classloader载入的同一个Class,所以造型的时候不会出现任何异常。而根据多形性,调用这个父类的方法时,真正执行的是这个Class(非父类Class)的覆盖了父类方法的方法。这些方法中也可以引用systemclassloader不能识别的Class,因为根据全盘负责原则,只要载入这个Class的classloader即我们自己定义的classloader能够定位和载入这些Class就行了。

这样我们就可以事先定义好一组接口或者基类并放入CLASSPATH中,然后在执行的时候动态的载入实现或者继承了这些接口或基类的子类。还不明白吗?让我们来想一想Servlet吧,web applicationserver能够载入任何继承了Servlet的Class并正确的执行它们,不管它实际的Class是什么,就是都把它们实例化成为一个ServletClass,然后执行Servlet的init,doPost,doGet和destroy等方法的,而不管这个Servlet是从web-inf/lib和web-inf/classes下由systemclassloader的子classloader(即定制的classloader)动态载入。说了这么多希望大家都明白了。在applet,ejb等容器中,都是采用了这种机制.

对于以上各种情况,希望大家实际编写一些example来实验一下。

最后我再说点别的,classloader虽然称为类加载器,但并不意味着只能用来加载Class,我们还可以利用它也获得图片,音频文件等资源的URL,当然,这些资源必须在CLASSPATH中的jar类库中或目录下。我们来看API的doc中关于ClassLoader的两个寻找资源和Class的方法描述吧:
        public URL getResource(String name)
        用指定的名字来查找资源,一个资源是一些能够被class代码访问的在某种程度上依赖于代码位置的数据(图片,音频,文本等等)。
            一个资源的名字是以'/'号分隔确定资源的路径名的。
             这个方法将先请求parentclassloader搜索资源,如果没有parent,则会在内置在虚拟机中的classloader(即bootstrapclassloader)的路径中搜索。如果失败,这个方法将调用findResource(String)来寻找资源。
        public static URL getSystemResource(String name)
            从用来载入类的搜索路径中查找一个指定名字的资源。这个方法使用system classloader来定位资源。即相当于ClassLoader.getSystemClassLoader().getResource(name)。

例如:
  System.out.println(ClassLoader.getSystemResource("java/lang/String.class"));
的结果为:
  jar:文件:/C:/j2sdk1.4.1_01/jre/lib/rt.jar!/java/lang/String.class
表明String.class文件在rt.jar的java/lang目录中。
因此我们可以将图片等资源随同Class一同打包到jar类库中(当然,也可单独打包这些资源)并添加它们到classloader的搜索路径中,我们就可以无需关心这些资源的具体位置,让class loader来帮我们寻找了!



jvm classLoaderarchitecture :

a, Bootstrap ClassLoader/启动类加载器
      主要负责jdk_home/lib目录下的核心 api 或-Xbootclasspath 选项指定的jar包装入工作.

b, Extension ClassLoader/扩展类加载器
      主要负责jdk_home/lib/ext目录下的jar包或-Djava.ext.dirs 指定目录下的jar包装入工作

c, System ClassLoader/系统类加载器
       主要负责java-classpath/-Djava.class.path所指的目录下的类与jar包装入工作.

b, User CustomClassLoader/用户自定义类加载器(java.lang.ClassLoader的子类)
       在程序运行期间,通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性.

类加载器的特性:

1, 每个ClassLoader都维护了一份自己的名称空间,同一个名称空间里不能出现两个同名的类。
2, 为了实现java安全沙箱模型顶层的类加载器安全机制, java默认采用了 "双亲委派的加载链 " 结构.
如下图:

 

Class Diagram:


类图中, BootstrapClassLoader是一个单独的java类, 其实在这里, 不应该叫他是一个java类。
因为, 它已经完全不用java实现了。

它是在jvm启动时, 就被构造起来的, 负责java平台核心库。(具体上面已经有介绍)

启动类加载实现 (其实我们不用关心这块, 但是有兴趣的, 可以研究一下 ):

bootstrap classLoader 类加载原理探索
www.javaeye.com/topic/136885

自定义类加载器加载一个类的步骤 :

 

 

 


ClassLoader 类加载逻辑分析, 以下逻辑是除 BootstrapClassLoader 外的类加载器加载流程:

java 代码
  1. // 检查类是否已被装载过
  2. Class c = findLoadedClass(name);
  3. if (c== null ){
  4.     // 指定类未被装载过
  5.     try {
  6.         if(parent !=null ) {
  7.             // 如果父类加载器不为空, 则委派给父类加载
  8.             c = parent.loadClass(name,false);
  9.         } else {
  10.             // 如果父类加载器为空, 则委派给启动类加载加载
  11.             c = findBootstrapClass0(name);
  12.         }
  13.     } catch(ClassNotFoundExceptione) {
  14.         // 启动类加载器或父类加载器抛出异常后, 当前类加载器将其
  15.         // 捕获, 并通过findClass方法, 由自身加载
  16.         c =findClass(name);
  17.     }
  18. }

用Class.forName加载类
Class.forName使用的是被调用者的类加载器来加载类的.
这种特性, 证明了java类加载器中的名称空间是唯一的, 不会相互干扰.
即在一般情况下, 保证同一个类中所关联的其他类都是由当前类的类加载器所加载的.

java 代码
  1. publicstaticClass forName(StringclassName)
  2.     throws ClassNotFoundException{
  3.     returnforName0(className,true, ClassLoader.getCallerClassLoader());
  4. }


  5. privatestaticnative Class forName0(String name,booleaninitialize,
  6. ClassLoader loader)
  7.     throwsClassNotFoundException;


上图中 ClassLoader.getCallerClassLoader就是得到调用当前forName方法的类的类加载器


线程上下文类加载器
java默认的线程上下文类加载器是系统类加载器(AppClassLoader).

java 代码
  1. // Now create the classloader to use to launch the application
  2. try{
  3.     loader= AppClassLoader.getAppClassLoader(extcl);
  4. } catch (IOExceptione) {
  5.    throw newInternalError(
  6. "Could not create application classloader" );
  7. }

  8. // Also set the contextclass loader for the primordial thread.
  9. Thread.currentThread().setContextClassLoader(loader);


以上代码摘自sun.misc.Launch的无参构造函数Launch()。

使用线程上下文类加载器, 可以在执行线程中, 抛弃双亲委派加载链模式, 使用线程上下文里的类加载器加载类.
典型的例子有, 通过线程上下文来加载第三方库jndi实现, 而不依赖于双亲委派.
大部分java app服务器(jboss,tomcat..)也是采用contextClassLoader来处理web服务。
还有一些采用 hotswap 特性的框架, 也使用了线程上下文类加载器, 比如 seasar (full stackframework in japenese).

线程上下文从根本解决了一般应用不能违背双亲委派模式的问题.
使java类加载体系显得更灵活.

随着多核时代的来临, 相信多线程开发将会越来越多地进入程序员的实际编码过程中. 因此,
在编写基础设施时, 通过使用线程上下文来加载类, 应该是一个很好的选择.

当然, 好东西都有利弊. 使用线程上下文加载类, 也要注意, 保证多根需要通信的线程间的类加载器应该是同一个,
防止因为不同的类加载器, 导致类型转换异常(ClassCastException).

自定义的类加载器实现
defineClass(String name, byte[]b, int off, int len,ProtectionDomain protectionDomain)
是java.lang.Classloader提供给开发人员, 用来自定义加载class的接口.

使用该接口, 可以动态的加载class文件.

例如,
在jdk中, URLClassLoader是配合findClass方法来使用defineClass,可以从网络或硬盘上加载class.

而使用类加载接口, 并加上自己的实现逻辑, 还可以定制出更多的高级特性.

比如,


一个简单的hot swap 类加载器实现:

java 代码
  1. importjava.io.File;
  2. importjava.io.FileInputStream;
  3. importjava.lang.reflect.Method;
  4. importjava.net.URL;
  5. importjava.net.URLClassLoader;


  6. publicclassHotSwapClassLoaderextends URLClassLoader{

  7.    public HotSwapClassLoader(URL[]urls) {
  8.        super(urls);
  9.    }

  10.    public HotSwapClassLoader(URL[]urls, ClassLoader parent) {
  11.        super(urls,parent);
  12.    }

  13.    public Class load(Stringname)
  14.          throwsClassNotFoundException{
  15.        returnload(name,false );
  16.    }

  17.    public Class load(Stringname,booleanresolve)
  18.          throwsClassNotFoundException{
  19.        if( null != super.findLoadedClass(name))
  20.            returnreload(name,resolve);

  21.        Class clazz = super.findClass(name);

  22.        if(resolve)
  23.            super.resolveClass(clazz);

  24.        returnclazz;
  25.    }

  26.    public Class reload(Stringname, booleanresolve)
  27.          throwsClassNotFoundException{
  28.        returnnew HotSwapClassLoader( super .getURLs(), super .getParent()).load(
  29.            name, resolve);
  30.    }
  31. }

 

java 代码
  1. publicclassA {
  2.    private B b;

  3.    public void setB(B b) {
  4.         this.b = b;
  5.    }

  6.    public B getB(){
  7.         returnb;
  8.    }
  9. }

 

java 代码
  1. publicclassB {}



这个类的作用是可以重新载入同名的类, 但是, 为了实现hotswap, 老的对象状态
需要通过其他方式拷贝到重载过的类生成的全新实例中来。(A类中的b实例)

而新实例所依赖的B类如果与老对象不是同一个类加载器加载的, 将会抛出类型转换异常(ClassCastException).

为了解决这种问题, HotSwapClassLoader自定义了load方法. 即当前类是由自身classLoader加载的,而内部依赖的类还是老对象的classLoader加载的.

java 代码
  1. publicclassTestHotSwap{
  2. public staticvoidmain(String args[]) {
  3.     A a =new A();
  4.     Bb = new B();
  5.    a.setB(b);

  6.    System.out.printf("A classLoader is %s\n",a.getClass().getClassLoader());
  7.    System.out.printf("B classLoader is %s\n",b.getClass().getClassLoader());
  8.    System.out.printf("A.b classLoader is %s\n" a.getB().getClass().getClassLoader());

  9.    HotSwapClassLoader c1 = newHotSwapClassLoader( newURL[]{newURL( "file:\\e:\\test\\")} ,a.getClass().getClassLoader());
  10.    Class clazz = c1.load(" test.hotswap.A");
  11.    Object aInstance = clazz.newInstance();

  12.    Method method1 = clazz.getMethod("setB", B.class);
  13.    method1.invoke(aInstance, b);

  14.    Method method2 = clazz.getMethod("getB", null);
  15.    Object bInstance = method2.invoke(aInstance,null);

  16.    System.out.printf("reloaded A.b classLoader is %s\n", bInstance.getClass().getClassLoader());
  17. }
  18. }



输出

A classLoader is sun.misc.Launcher$AppClassLoader@19821f
B classLoader is sun.misc.Launcher$AppClassLoader@19821f
A.b classLoader is sun.misc.Launcher$AppClassLoader@19821f
reloaded A.b classLoader issun.misc.Launcher$AppClassLoader@19821f


何时使用Thread.getContextClassLoader()?

这是一个很常见的问题,但答案却很难回答。这个问题通常在需要动态加载类和资源的系统编程时会遇到。总的说来动态加载资源时,往往需要从三种类加载器里选择:系统或说程序的类加载器、当前类加载器、以及当前线程的上下文类加载器。在程序中应该使用何种类加载器呢?

   系统类加载器通常不会使用。此类加载器处理启动应用程序时classpath指定的类,可以通过ClassLoader.getSystemClassLoader()来获得。所有的ClassLoader.getSystemXXX()接口也是通过这个类加载器加载的。一般不要显式调用这些方法,应该让其他类加载器代理到系统类加载器上。由于系统类加载器是JVM最后创建的类加载器,这样代码只会适应于简单命令行启动的程序。一旦代码移植到EJB、Web应用或者Java Web Start应用程序中,程序肯定不能正确执行。

   因此一般只有两种选择,当前类加载器和线程上下文类加载器。当前类加载器是指当前方法所在类的加载器。这个类加载器是运行时类解析使用的加载器,Class.forName(String)和Class.getResource(String)也使用该类加载器。代码中X.class的写法使用的类加载器也是这个类加载器。

   线程上下文类加载器在Java 2(J2SE)时引入。每个线程都有一个关联的上下文类加载器。如果你使用newThread()方式生成新的线程,新线程将继承其父线程的上下文类加载器。如果程序对线程上下文类加载器没有任何改动的话,程序中所有的线程将都使用系统类加载器作为上下文类加载器。Web应用和Java企业级应用中,应用服务器经常要使用复杂的类加载器结构来实现JNDI(Java命名和目录接口)、线程池、组件热部署等功能,因此理解这一点尤其重要。

   为什么要引入线程的上下文类加载器?将它引入J2SE并不是纯粹的噱头,由于Sun没有提供充分的文档解释说明这一点,这使许多开发者很糊涂。实际上,上下文类加载器为同样在J2SE中引入的类加载代理机制提供了后门。通常JVM中的类加载器是按照层次结构组织的,目的是每个类加载器(除了启动整个JVM的原初类加载器)都有一个父类加载器。当类加载请求到来时,类加载器通常首先将请求代理给父类加载器。只有当父类加载器失败后,它才试图按照自己的算法查找并定义当前类。

   有时这种模式并不能总是奏效。这通常发生在JVM核心代码必须动态加载由应用程序动态提供的资源时。拿JNDI为例,它的核心是由JRE核心类(rt.jar)实现的。但这些核心JNDI类必须能加载由第三方厂商提供的JNDI实现。这种情况下调用父类加载器(原初类加载器)来加载只有其子类加载器可见的类,这种代理机制就会失效。解决办法就是让核心JNDI类使用线程上下文类加载器,从而有效的打通类加载器层次结构,逆着代理机制的方向使用类加载器。(bad translation, cuiyi add)

   顺便提一下,XML解析API(JAXP)也是使用此种机制。当JAXP还是J2SE扩展时,XML解析器使用当前累加载器方法来加载解析器实现。但当JAXP成为J2SE核心代码后,类加载机制就换成了使用线程上下文加载器,这和JNDI的原因相似。

   好了,现在我们明白了问题的关键:这两种选择不可能适应所有情况。一些人认为线程上下文类加载器应成为新的标准。但这在不同JVM线程共享数据来沟通时,就会使类加载器的结构乱七八糟。除非所有线程都使用同一个上下文类加载器。而且,使用当前类加载器已成为缺省规则,它们广泛应用在类声明、Class.forName等情景中。即使你想尽可能只使用上下文类加载器,总是有这样那样的代码不是你所能控制的。这些代码都使用代理到当前类加载器的模式。混杂使用代理模式是很危险的。

   更为糟糕的是,某些应用服务器将当前类加载器和上下文类加器分别设置成不同的ClassLoader实例。虽然它们拥有相同的类路径,但是它们之间并不存在父子代理关系。想想这为什么可怕:记住加载并定义某个类的类加载器是虚拟机内部标识该类的组成部分,如果当前类加载器加载类X并接着执行它,如JNDI查找类型为Y的数据,上下文类加载器能够加载并定义Y,这个Y的定义和当前类加载器加载的相同名称的类就不是同一个,使用隐式类型转换就会造成异常。

    这种混乱的状况还将在Java中存在很长时间。在J2SE中还包括以下的功能使用不同的类加载器:

    *JNDI使用线程上下文类加载器

    *Class.getResource()和Class.forName()使用当前类加载器

    *JAXP使用上下文类加载器

    *java.util.ResourceBundle使用调用者的当前类加载器

    *URL协议处理器使用java.protocol.handler.pkgs系统属性并只使用系统类加载器。

    *Java序列化API缺省使用调用者当前的类加载器

   这些类加载器非常混乱,没有在J2SE文档中给以清晰明确的说明。


Which ClassLoader should you use?


Find a way out of the ClassLoader maze

System, current, context? WhichClassLoader should you use?

 

When should I useThread.getContextClassLoader()?

Although not frequently asked, thisquestion is rather tough to correctly answer. It usually comes upduring framework programming, when a good deal of dynamic class andresource loading goes on. In general, when loading a resourcedynamically, you can choose from at least three classloaders: thesystem (also referred to as theapplication)classloader, the current classloader, and the currentthreadcontext classloader. The question above refers tothe latter. Which classloader is the right one?

One choice I dismiss easily: thesystem classloader. This classloader handles -classpath and isprogrammatically accessible as ClassLoader.getSystemClassLoader().All ClassLoader.getSystemXXX() API methods are also routed throughthis classloader. You should rarely write code that explicitly usesany of the previous methods and instead let other classloadersdelegate to the system one. Otherwise, your code will only work insimple command-line applications, when the system classloader isthe last classloader created in the JVM. As soon as you move yourcode into an Enterprise JavaBean, a Web application, or a Java Web Startapplication, things are guaranteed to break.

 

So, now we are down to two choices:current and context classloaders. By definition, a currentclassloader loads and defines the class to which your currentmethod belongs. This classloader is implied when dynamic linksbetween classes resolve at runtime, and when you use theone-argument version of Class.forName(), Class.getResource(), andsimilar methods. It is also used by syntactic constructs likeX.class class literals (see "Geta Load of That Name!" for moredetails).

 

Thread context classloaders wereintroduced in Java 2 Platform, Standard Edition (J2SE). EveryThread has a context classloader associated with it (unless it wascreated by native code). It is set via theThread.setContextClassLoader() method. If you don't invoke thismethod following a Thread's construction, the thread will inheritits context classloader from its parent Thread. If you don't doanything at all in the entire application, all Threads will end upwith the system classloader as their context classloader. It isimportant to understand that nowadays this is rarely the case sinceWeb and Java 2 Platform, Enterprise Edition (J2EE) applicationservers utilize sophisticated classloader hierarchies for featureslike Java Naming and Directory Interface (JNDI), thread pooling,component hot redeployment, and so on.

 

Why do thread context classloadersexist in the first place? They were introduced in J2SE without muchfanfare. A certain lack of proper guidance and documentation fromSun Microsystems likely explains why many developers find themconfusing.

In truth, context classloadersprovide a back door around the classloading delegation scheme alsointroduced in J2SE. Normally, all classloaders in a JVM areorganized in a hierarchy such that every classloader (except fortheprimordial classloader that bootstraps the entire JVM)has a single parent. When asked to load a class, every compliantclassloader is expected to delegate loading to its parent first andattempt to define the class only if the parentfails.

Sometimes this orderly arrangementdoes not work, usually when some JVM core code must dynamicallyload resources provided by application developers. Take JNDI forinstance: itsguts are implemented by bootstrap classes in rt.jar(starting with J2SE 1.3), but these core JNDI classes mayload JNDI providers implemented by independent vendors andpotentially deployed in the application's -classpath. This scenariocalls for a parent classloader (the primordial one in this case) toload a class visible to one of its child classloaders(the systemone,for example). Normal J2SE delegation does not work, and theworkaround is to make the core JNDI classes use thread contextloaders, thus effectively "tunneling" through the classloaderhierarchy in the direction opposite to the properdelegation.

 

By the way, the previous paragraphmay have reminded you of something else: Java API for XML Parsing(JAXP). Yes, when JAXP was just a J2SE extension, the XML parserfactories used the current classloader approach for bootstrappingparser implementations. When JAXP was made part of the J2SE 1.4core, the classloading changed to use thread context classloaders,in complete analogy with JNDI (and confusing many programmers alongthe way). See what I mean by lack of guidance fromSun?

After this introduction, I have cometo the crux of the matter: neither of the remaining two choices isthe right one under all circumstances. Some believe that threadcontext classloaders should become the new standard strategy. This,however, creates a very messy classloading picture if various JVMthreads communicate via shared data, unless all of them use thesame context loader instance. Furthermore, delegating to thecurrent classloader is already a legacy rule in some existingsituations like class literals or explicit calls to Class.forName()(which is why, by the way, I recommend (again, see "Geta Load of That Name!") avoiding the one-argumentversion of this method). Even if you make an explicit effort to useonly context loaders whenever you can, there will always be somecode not under your control that delegates to the current loader.This uncontrolled mixing of delegation strategies sounds ratherdangerous.

 

To make matters worse, certainapplication servers set context and current classloaders todifferent ClassLoader instances that have thesame classpaths and yet arenot related as adelegation parent and child. Take a second to think about whythis is particularly horrendous. Remember that the classloader thatloads and defines a class is part of the internal JVM's ID for thatclass. If the current classloader loads a class X that subsequentlyexecutes, say, a JNDI lookup for some data of type Y, the contextloader could load and define Y. This Y definition will differ fromthe one by the same name but seen by the current loader. Enterobscure class cast and loader constraint violationexceptions.

This confusion will probably staywith Java for some time. Take any J2SE API with dynamic resourceloading of any kind and try to guess which loading strategy ituses. Here is a sampling:

·                      JNDIuses context classloaders

·                      Class.getResource() and Class.forName() use the currentclassloader

·                      JAXPuses context classloaders (as of J2SE1.4)

·                      java.util.ResourceBundle uses the caller's currentclassloader

·                      URLprotocol handlers specified via java.protocol.handler.pkgs systemproperty are looked up in the bootstrap and system classloadersonly

·                      JavaSerialization API uses the caller's current classloader bydefault


自定义ClassLoader

很多时候人们会使用一些自定义的ClassLoader 

,而不是使用系统的Class Loader。大多数时候人们这样做的原因是,他们在编译时无法预知运行时会需要那些Class。特别是在那些appserver中,比如tomcat,Avalon-phonix,Jboss中。或是程序提供一些plug-in的功能,用户可以在程序编译好之后再添加自己的功能,比如ant, jxta-shell等。定制一个ClassLoader很简单,一般只需要理解很少的几个方法就可以完成。
一个最简单的自定义的ClassLoader从ClassLoader类继承而来。这里我们要做一个可以在运行时指定路径,加载这个路径下的class的ClassLoader。
通常我们使用ClassLoader.loadClass(String):Class方法,通过给出一个类名,就会得到一个相应的Class实例。因此只要小小的改动这个方法,就可以实现我们的愿望了。
源码:
  1. protected synchronized Class loadClass(String name, boolean resolve)    throws ClassNotFoundException       
  2.     // First, check if the class has already been loaded    
  3.     Class findLoadedClass(name);    
  4.     if (c == null{
  5.         try {
  6.            if (parent != null{
  7.               parent.loadClass(name, false);
  8.            }else{
  9.               findBootstrapClass0(name);
  10.            }
  11.         }catch(ClassNotFoundException e){
  12.             // If still not found, then call findClass in order
  13.             // to find the class.            
  14.            findClass(name);
  15.         }
  16.     }
  17.     if (resolve) {
  18.       resolveClass(c);
  19.     }
  20.     return c;
  21. }

Source from ClassLoader.java

First,check JavaAPI doc:上面指出了缺省的loadClass方法所做的几个步骤。
1.    调用findLoadedClass(String):Class 检查一下这个class是否已经被加载过了,由于JVM 规范规定ClassLoader可以cache它所加载的Class,因此如果一个class已经被加载过的话,直接从cache中获取即可。
2.    调用它的parent 的loadClass()方法,如果parent为空,这使用JVM内部的class loader(即著名的bootstrap classloader)。
3.    如果上面两步都没有找到,调用findClass(String)方法来查找并加载这个class。
后面还有一句话,在Java 1.2版本以后,鼓励用户通过继承findClass(String)方法实现自己的class loader而不是继承loadClass(String)方法。
既然如此,那么我们就先这么做:)
  1. public class AnotherClassLoader extends ClassLoader {
  2.     private String baseDir;private static final Logger LOG 
  3.          Logger.getLogger(AnotherClassLoader.class);    
  4.     public AnotherClassLoader (ClassLoader parent, String baseDir) {
  5.            super(parent);
  6.            this.baseDir baseDir;
  7.     }
  8.     protected Class findClass(String name)
  9.             throws ClassNotFoundException {
  10.         LOG.debug("findClass " name);
  11.         byte[] bytes loadClassBytes(name);
  12.         Class theClass defineClass(name, bytes, 0, bytes.length);//A
  13.         if (theClass == null)
  14.             throw new ClassFormatError();
  15.         return theClass;
  16.     }
  17.     private byte[] loadClassBytes(String className) throws
  18.         ClassNotFoundException {
  19.         try {
  20.             String classFile getClassFile(className);
  21.             FileInputStream fis new FileInputStream(classFile);
  22.             FileChannel fileC fis.getChannel();
  23.             ByteArrayOutputStream baos new ByteArrayOutputStream();
  24.             WritableByteChannel outC Channels.newChannel(baos);
  25.             ByteBuffer buffer ByteBuffer.allocateDirect(1024);
  26.             while (true{
  27.                 int fileC.read(buffer);
  28.                 if (i == || == -1) {
  29.                     break;
  30.                 }
  31.                 buffer.flip();
  32.                 outC.write(buffer);
  33.                 buffer.clear();
  34.             }
  35.             fis.close();
  36.             return baos.toByteArray();
  37.         catch (IOException fnfe) {
  38.             throw new ClassNotFoundException(className);
  39.         }
  40.     }
  41.     private String getClassFile(String name) {
  42.         StringBuffer sb new StringBuffer(baseDir);
  43.         name name.replace('.', File.separatorChar) ".class";
  44.         sb.append(File.separator name);
  45.         return sb.toString();
  46.     }
  47. }

[i]Ps:这里使用了一些JDK1.4的nio的代码:)[/i]
很简单的代码,关键的地方就在A处,我们使用了defineClass方法,目的在于把从class文件中得到的二进制数组转换为相应的Class实例。defineClass是一个native的方法,它替我们识别class文件格式,分析读取相应的数据结构,并生成一个class实例。

还没完呢,我们只是找到了发布在某个目录下的class,还有资源呢。我们有时会用Class.getResource():URL来获取相应的资源文件。如果仅仅使用上面的ClassLoader是找不到这个资源的,相应的返回值为null。

同样我们看一下原来的ClassLoader内部的结构。
  1. public java.net.URL getResource(String name) {
  2.         name resolveName(name);
  3.         ClassLoader cl getClassLoader0();//这里
  4.         if (cl==null{
  5.             // system class.
  6.             return ClassLoader.getSystemResource(name);
  7.         }
  8.         return cl.getResource(name);}


原来是使用加载这个class的那个classLoader获取得资源。

  1. public URL getResource(String name) {
  2.     URL url;
  3.     if (parent != null{
  4.         url parent.getResource(name);
  5.     else {
  6.         url getBootstrapResource(name);
  7.     }
  8.     if (url == null{
  9.         url findResource(name);//这里
  10.     }
  11.     return url;
  12. }




这样看来只要继承findResource(String)方法就可以了。修改以下我们的代码:

  1. //新增的一个findResource方法
  2. protected URL findResource(String name) {
  3.         LOG.debug("findResource " name);
  4.         try {
  5.             URL url super.findResource(name);
  6.             if (url != null)
  7.                 return url;
  8.             url new URL("file:///" converName(name));
  9.             //简化处理,所有资源从文件系统中获取
  10.             return url;
  11.         catch (MalformedURLException mue) {
  12.             LOG.error("findResource"mue);
  13.             return null;
  14.         }
  15. }
  16. private String converName(String name) {
  17.         StringBuffer sb new StringBuffer(baseDir);
  18.         name name.replace('.', File.separatorChar);
  19.         sb.append(File.separator name);
  20.         return sb.toString();
  21. }


原创粉丝点击