【转】Jave 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自身实现的。你们能够经过执行如下代码来得到bootstrap classloader加载了那些核心类库:
   URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
   for (int i = 0; i < urls.length; i++) {
     System.out.println(urls[i].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和system classloader都是可见的。在这个实例上调用方法getParent()老是返回空值null,由于引导加载器bootstrap classloader不是一个真正的ClassLoader实例。因此当你们执行如下代码时:
   System.out.println(System.getProperty("java.ext.dirs"));
   ClassLoader extensionClassloader=ClassLoader.getSystemClassLoader().getParent();
   System.out.println("the parent of extension classloader : "+extensionClassloader.getParent());
结果为:
C:\j2sdk1.4.1_01\jre\lib\ext
the parent of extension classloader : null
extension classloader是system classloader的parent,而bootstrap classloader是extension classloader的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,它与parent classloader类不是继承关系)寻找,只有在parent找不到的时候才从本身的类路径中去寻找。此外类加载还采用了cache机制,也就是若是 cache中保存了这个Class就直接返回它,若是没有才从文件中读取和转换成Class,并存入cache,这就是为何咱们修改了Class可是必须从新启动JVM才能生效的缘由。


每一个ClassLoader加载Class的过程是:
1.检测此Class是否载入过(即在cache中是否有此Class),若是有到8,若是没有到2
2.若是parent classloader不存在(没有parent,那parent必定是bootstrap classloader了),到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,最后才是system classloader。你们会发现加载的Class越是重要的越在靠前面。这样作的缘由是出于安全性的考虑,试想若是system classloader“亲自”加载了一个具备破坏性的“java.lang.System”类的后果吧。这种委托机制保证了用户即便具备一个这样的类,也把它加入到了类路径中,可是它永远不会被载入,由于这个类老是由bootstrap classloader来加载的。你们能够执行一下如下的代码:
   System.out.println(System.class.getClassLoader());
将会看到结果是null,这就代表java.lang.System是由bootstrap classloader加载的,由于bootstrap classloader不是一个真正的ClassLoader实例,而是由JVM实现的,正如前面已经说过的。

下面就让咱们来看看JVM是如何来为咱们来创建类加载器的结构的:
sun.misc.Launcher,顾名思义,当你执行java命令的时候,JVM会先使用bootstrap classloader载入并初始化一个Launcher,执行下来代码:
  System.out.println("the Launcher's classloader is "+sun.misc.Launcher.getLauncher().getClass().getClassLoader());
结果为:
  the Launcher's classloader is null (由于是用bootstrap classloader加载,因此class loader为null)
Launcher 会根据系统和命令设定初始化好class loader结构,JVM就用它来得到extension classloader和system classloader,并载入全部的须要载入的Class,最后执行java命令指定的带有静态的main方法的Class。extension classloader其实是sun.misc.LauncherExtClassLoadersystemclassloadersun.misc.LauncherAppClassLoader类的一个实例。而且都是 java.net.URLClassLoader的子类。

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

Launcher的部分代码:
Java代码
html

 
01. public class Launcher  {
02. public Launcher() {
03. ExtClassLoader extclassloader;
04. try {
05. //初始化extension classloader
06. extclassloader = ExtClassLoader.getExtClassLoader();
07. } catch(IOException ioexception) {
08. throw new InternalError("Could not create extension class loader");
09. }
10. try {
11. //初始化system classloader,parent是extension classloader
12. loader = AppClassLoader.getAppClassLoader(extclassloader);
13. } catch(IOException ioexception1) {
14. throw new InternalError("Could not create application class loader");
15. }
16. //将system classloader设置成当前线程的context classloader(将在后面加以介绍)
17. Thread.currentThread().setContextClassLoader(loader);
18. ......
19. }
20. public ClassLoader getClassLoader() {
21. //返回system classloader
22. return loader;
23. }
24. }
25.  
26. extension classloader的部分代码:
27. static class Launcher$ExtClassLoader extends URLClassLoader {
28.  
29. public static Launcher$ExtClassLoader getExtClassLoader()
30. throws IOException
31. {
32. File afile[] = getExtDirs();
33. return (Launcher$ExtClassLoader)AccessController.doPrivileged(new Launcher$1(afile));
34. }
35. private static File[] getExtDirs() {
36. //得到系统属性“java.ext.dirs”
37. String s = System.getProperty("java.ext.dirs");
38. File afile[];
39. if(s != null) {
40. StringTokenizer stringtokenizer = new StringTokenizer(s, File.pathSeparator);
41. int i = stringtokenizer.countTokens();
42. afile = new File;
43. for(int j = 0; j < i; j++)
44. afile[j] = new File(stringtokenizer.nextToken());
45.  
46. } else {
47. afile = new File[0];
48. }
49. return afile;
50. }
51. }
52.  
53. system classloader的部分代码:
54. static class Launcher$AppClassLoader extends URLClassLoader
55. {
56.  
57. public static ClassLoader getAppClassLoader(ClassLoader classloader)
58. throws IOException
59. {
60. //得到系统属性“java.class.path”
61. String s = System.getProperty("java.class.path");
62. File afile[] = s != null ? Launcher.access$200(s) : new File[0];
63. return (Launcher$AppClassLoader)AccessController.doPrivileged(new Launcher$2(s, afile, classloader));
64. }
65. }

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

  这里怎么又出来一个context classloader呢?它有什么用呢?咱们在创建一个线程Thread的时候,能够为这个线程经过setContextClassLoader方法来指定一个合适的classloader做为这个线程的context classloader,当此线程运行的时候,咱们能够经过getContextClassLoader方法来得到此context classloader,就能够用它来载入咱们所须要的Class。默认的是system classloader。利用这个特性,咱们能够“打破”classloader委托机制了,父classloader能够得到当前线程的context classloader,而这个context classloader能够是它的子classloader或者其余的 classloader,那么父classloader就能够从其得到所需的 Class,这就打破了只能向父classloader请求的限制了。这个机制能够知足当咱们的classpath是在运行时才肯定,并由定制的 classloader加载的时候,由system classloader(即在jvm classpath中)加载的class能够经过context classloader得到定制的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 new URLClassLoader for the specified URLs using the default delegation parent ClassLoader.
URLClassLoader(URL[] urls, ClassLoader parent)
         Constructs a new URLClassLoader for the given URLs.
其中URL[] urls就是咱们要设置的类搜索路径,parent就是这个classloader的parent classloader,默认的是system classloader。


好,如今咱们可以动态的载入Class了,这样咱们就能够利用newInstance方法来得到一个Object。但咱们如何将此Object造型呢?能够将此Object造型成它自己的Class吗?

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

   public static int compile(String as[]);

   public static int compile(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初始化的system classloader载入的,根据全盘负责原则,编译器在解析这个java源文件时所发现的它所依赖和引用的全部Class也将由system classloader载入,若是system classloader不能载入某个Class时,编译器将抛出一个“cannot resolve symbol”错误。

因此首先编译就通不过,也就是编译器没法编译一个引用了不在CLASSPATH中的未知Class的java源文件,而因为拼写错误或者没有把所需类库放到CLASSPATH中,你们必定常常看到这个“cannot resolve symbol”这个编译错误吧!

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

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

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

还不死心!再尝试一下这种状况,咱们把这个Class也放入到CLASSPATH中,让system classloader可以识别和载入。而后咱们经过本身的classloader来从指定的class文件中载入这个Class(不可以委托 parent载入,由于这样会被system classloader从CLASSPATH中将其载入),而后实例化一个Object,并造型成这个Class,这样JVM也识别这个Class(由于 system classloader可以定位和载入这个Class从CLASSPATH中),载入的也不是CLASSPATH中的这个Class,而是从 CLASSPATH外动态载入的,这样总行了吧!十分不幸的是,这时会出现“java.lang.ClassCastException”违例。

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

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

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

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

对于以上各类状况,但愿你们实际编写一些example来实验一下。

最后我再说点别的, classloader虽然称为类加载器,但并不意味着只能用来加载Class,咱们还能够利用它也得到图片,音频文件等资源的URL,固然,这些资源必须在CLASSPATH中的jar类库中或目录下。咱们来看API的doc中关于ClassLoader的两个寻找资源和Class的方法描述吧:
        public URL getResource(String name)
        用指定的名字来查找资源,一个资源是一些可以被class代码访问的在某种程度上依赖于代码位置的数据(图片,音频,文本等等)。
               一个资源的名字是以'/'号分隔肯定资源的路径名的。
               这个方法将先请求parent classloader搜索资源,若是没有parent,则会在内置在虚拟机中的classloader(即bootstrap classloader)的路径中搜索。若是失败,这个方法将调用findResource(String)来寻找资源。
        public static URL getSystemResource(String name)
               从用来载入类的搜索路径中查找一个指定名字的资源。这个方法使用system class loader来定位资源。即至关于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类库中(固然,也可单独打包这些资源)并添加它们到class loader的搜索路径中,咱们就能够无需关心这些资源的具体位置,让class loader来帮咱们寻找了! java

本文由papa于2009-06-02 00:18:33.0在http://www.faceye.com发布web

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

  这里怎么又出来一个context classloader呢?它有什么用呢?咱们在创建一个线程Thread的时候,能够为这个线程经过setContextClassLoader方法来指定一个合适的classloader做为这个线程的context classloader,当此线程运行的时候,咱们能够经过getContextClassLoader方法来得到此context classloader,就能够用它来载入咱们所须要的Class。默认的是system classloader。利用这个特性,咱们能够“打破”classloader委托机制了,父classloader能够得到当前线程的context classloader,而这个context classloader能够是它的子classloader或者其余的 classloader,那么父classloader就能够从其得到所需的 Class,这就打破了只能向父classloader请求的限制了。这个机制能够知足当咱们的classpath是在运行时才肯定,并由定制的 classloader加载的时候,由system classloader(即在jvm classpath中)加载的class能够经过context classloader得到定制的classloader并加载入特定的class(一般是抽象类和接口,定制的classloader中是其实现),例如web应用中的servlet就是用这种机制加载的.bootstrap

Reference: http://www.cnblogs.com/yangy608/archive/2011/07/23/2114900.htmlapi

相关文章
相关标签/搜索