classloader学习总结

来源:互联网 发布:linux o nonblock 编辑:程序博客网 时间:2024/05/05 14:35

来源:http://grid-qian.javaeye.com/blog/139561

classloader学习总结

关键字: classloader

没 想到自己的这篇总结会隔了这么久才发上来。一来工作比较忙,二来孩子有点闹,回家干不了啥活。于是就拖了这么久。其实,说起来网上介绍 classloader的文章很多,没必要再发一篇。不过,后来想想,还是自己再写一篇。一是对自己学习的一个总结,二是给大家多一个角度来学习 classloader。 开篇先列一下我的参考文章吧。网上相关文章太多,看了好久,筛选出来这几篇。首先就是最重要的IBM的developwork上的两个系列文章。我觉得 IBM的这个技术网站做的太棒了,很多文章都是学习的好帮手。我遇到什么问题都喜欢先去搜一下。然后还有其他几篇不错的文章,包括robbin的一篇。还 有就是《深入JVM》这本经典的书了。建议有时间,学习JAVA的人都应该去看看。 http://www.ibm.com/developerworks/cn/java/j-dclp1/                                                                                                                                 

http://www.ibm.com/developerworks/cn/java/j-dyn0429/

http://www.javaeye.com/topic/136427                                                                                                           

http://www.javaeye.com/topic/11?page=1                                              

http://www.jdon.com/article/15456.html                                                                                                                 

http://blog.bcchinese.net/shiaohuazhang/archive/2004/10/13/2715.aspx 一 classloader基础知识 我们要想从一个class文件得到一个在JVM中可以使用的类,需要经过一个复杂的过程。首先,JVM通过classloader机制装载一个JAVA类 型,然后连接,然后初始化。即装载-->连接(验证-->准备-->解析)-->初始化。经过这样一个过程,我们才能得到一个在 JVM中可以使用的JAVA类。连接包括验证,准备,解析。下面这个图表示了这个过程。从IBM的文章中截过来的,所以是英文,不好意思了。

类装入的阶段

装载就是把二进制形式的JAVA类型读入到JVM中。它为类对象提供了非常基本的内存结构。根据JAVA API的定义:类装载器是负责装载类的对象。ClassLoader 类是一个抽象类。如果给定类的二进制名字,那么类装载器会试图查找或生成构成类定义的数据。一般策略是将名称转换为某个文件名,然后从文件系统读取该名称的“类文件”。每个Class对象都包含一个对定义它的 ClassLoader引用。数组类的 Class 对象不是由类装载器创建的,而是由 Java 运行时根据需要自动创建。数组类的类装载器由 Class.getClassLoader()返回,该装载器与其元素类型的类装载器是相同的;如果该元素类型是基本类型,则该数组类没有类装载器。

装载由三个基本动作组成: 1 通过该类型的完全限定名产生一个代表该类型的二进制数据流 2 解析这个二进制数据流为方法区内的内部数据结构 3 创建一个表示该类型的java.lang.Class类的实例 JVM的classloader可以采用两种方式来装载JAVA类。一种是预先装载的方式来装载类;一种是用的时候再加载。但无论预先装载还是用的时候再 装载,装载过程中的错误,只有当程序中第一次主动使用该类时才报告错误:LinkageError等。如果该类一直没有被使用,那么就一直不会报告错误。 连接就是把这种已经读入得二进制形式的类型数据合并到JVM得运行时状态中去。验证确保JAVA类型数据格式正确并且适于JVM使用。准备负责为该类型分 配它所需得内存。解析负责把常量池中得符号引用转换成直接引用。解析这一步可以推迟到程序运行时真正使用这个符号时再去解析它。 初始化是在连接之后,JVM第一次主动使用该类型时进行的。所谓主动使用包括以下几种情况: 创建类的新实例时(new指令或通过不明确的创建,反射,克隆或反序列化) 调用类的静态方法时 使用类的静态字段,或对该字段赋值时(final修饰的静态字段除外) 初始化某个类的子类时 JVM启动时某个被标明为启动类的类即含有main()方法的类 类的初始化要求其超类先被初始化,但对于接口来说,并不是这样。只有当接口中所声明的非常量字段被使用时,该接口才被初始化。也就是说接口初始化时并不要 求其超类先初始化。在准备阶段,JVM为类变量分配内存,设置默认初始值。这个默认值是JAVA语言对每种类型的默认值。而在初始化阶段会给类变量赋予真 正的初始值。这个初始值才是程序员编程时指定的类变量的初始值。当一个类有超类的时候,它会先初始化这个超类。初始化之后,程序可以访问类的静态字段,调 用类的静态方法,或创建类的实例。 通过连接,可以把类的符号引用转换成直接引用,这时候要检查正确性和权限。JAVA语言的动态扩展性就是在连接过程中体现的。JAVA程序可以在运行时决 定连接哪个类型。这样就实现了JAVA的动态扩展性。有两种方法:一种是使用java.lang.Class的forName()方法;一种是采用用户自 定义的类装载器的loadClass()方法。第一种方法的优点在于它得到的类型一定是装载并初始化的;而第二种的只装载不一定初始化。但第二种方法更灵 活,可以实现一些特定的功能。比如说安全性或需要加载一些特定的类型如从网上下载的类型。 每一个classloader都维护属于自己的命名空间,在同一个命名空间里,两个类名字不能相同。命名空间由所有以此装载器为创始类装载器的类组成。不 同命名空间的两个类是不可见的,但只要得到类所对应的Class对象的reference,还是可以访问另一命名空间的类。为了实现JAVA的安全沙箱模 型顶层的类加载器安全机制,classloader采用双亲委派模型。(JAVA的安全沙箱机制在前一篇关于classloader的预备知识的文章里有 介绍)

一个例子,测试你所使用的JVM的ClassLoader

java 代码
  1. /*LoaderSample1.java*/  
  2. public class LoaderSample1 {       
  3.       public static void main(String[] args) {           
  4.             Class c;           
  5.             ClassLoader cl;   
  6.             cl = ClassLoader.getSystemClassLoader();           
  7.             System.out.println(cl);   
  8.             while (cl != null) {               
  9.                   cl = cl.getParent();               
  10.                   System.out.println(cl);           
  11.             }           
  12.             try {               
  13.                    c = Class.forName("java.lang.Object");               
  14.                    cl = c.getClassLoader();               
  15.                    System.out.println("java.lang.Object's loader is " + cl);               
  16.                    c = Class.forName("LoaderSample1");               
  17.                    cl = c.getClassLoader();               
  18.                    System.out.println("LoaderSample1's loader is " + cl);           
  19.             } catch (Exception e) {   
  20.                    e.printStackTrace();           
  21.             }       
  22.       }   
  23. }   

在我的机器上(Sun Java 1.4.2)的运行结果

sun.misc.Launcher$AppClassLoader@1a0c10fsun.misc.Launcher$ExtClassLoader@e2eec8null java.lang.Object's loader is nullLoaderSample1's loader is sun.misc.Launcher$AppClassLoader@1a0c10f

第一行表示,系统类装载器实例化自类 sun.misc.Launcher$AppClassLoader                                                                                  

第二行表示,系统类装载器的parent实例化自类sun.misc.Launcher$ExtClassLoader                                                                    

第三行表示,系统类装载器parent的parent为 bootstrap                                                                                                                          

第四行表示,核心类java.lang.Object是由bootstrap装载的

另一个例子,演示了一个命名空间的类如何使用另一命名空间的类。在例子中,LoaderSample2由系统类装载器装 载,LoaderSample3由自定义的装载器loader负责装载,两个类不在同一命名空间,但LoaderSample2得到了 LoaderSample3所对应的Class对象的reference,所以它可以访问LoaderSampl3中公共的成员(如age)。运行:java LoaderSample2

java 代码
  1. /*LoaderSample2.java*/  
  2. import java.net.*;   
  3. import java.lang.reflect.*;   
  4. public class LoaderSample2 {       
  5.       public static void main(String[] args) {           
  6.             try {               
  7.                  String path = System.getProperty("user.dir");                 
  8.                  URL[] us = {new URL("file://" + path + "/sub/")};               
  9.                  ClassLoader loader = new URLClassLoader(us);               
  10.                  Class c = loader.loadClass("LoaderSample3");               
  11.                  Object o = c.newInstance();               
  12.                  Field f = c.getField("age");               
  13.                  int age = f.getInt(o);               
  14.                  System.out.println("age is " + age);           
  15.             } catch (Exception e) {               
  16.                  e.printStackTrace();           
  17.             }       
  18.       }   
  19. }   
  20.   
  21.   
  22. /*Loadersample3.java*/  
  23. public class LoaderSample3 {    
  24.       static {           
  25.            System.out.println("LoaderSample3 loaded");       
  26.       }       
  27.       public int age = 30;   
  28. }   

LoaderSample3 loaded age is 30

从运行结果中可以看出,在类LoaderSample2中可以创建处于另一命名空间的类LoaderSample3中的对象并可以访问其公共成员age。

二 classloader实现机制 类装载器采用双亲委派模型。在建立用户自定义的类装载器时可以指定其双亲。如果没有指定,则默认的是把系统类装载器作为其双亲。如果向用户自定义的装载器的构造方法里传递null,则引导装载器就是双亲。 classloader依次从缓存,双亲,自己来寻找类。classloader首先判断要求它装入的类是否与过去装入的类相同。如果相同,就返回上次返 回的类(即保存在缓存中的类)。如果不是,就把装入类的机会交给父类。这两步递归地以深度优先的方式重复。如果父类返回 null(或抛出 ClassNotFoundException),那么类装入器会在自己的路径中寻找类的源。 因为父类类装入器总是先得到装入类的机会,所以classloader装入的类最靠近根。这意味着所有核心引导类都是由引导装入器装入的,这就保证装入了类(例如 java.lang.Object)的正确版本。这也可以让类装入器看到自己或父类或祖先装入的类,但是不能看到子女装入的类。

类装入器委托模型
与其他类装入器不同,引导类装入器(也称作基本(primordial) 类装入器)不能由 Java 代码实例化。(通常是因为它是作为 VM 本身的一部分实现的。)这个类装入器可以从启动的类路径装入核心系统类,通常是位于 jre/lib 目录的 JAR 文件。但是能用 -Xbootclasspath 命令行选项修改这个类路径(稍后介绍)。 扩展(extension) 类装入器(也称作标准扩展 类装入器)是引导类装入器的一个孩子。它的主要职责是从扩展目录装入类,通常位于 jre/lib/ext 目录。这提供了简单地访问新扩展的能力,例如不同的安全扩展,不需要修改用户的类路径即可实现。 系统(system) 类装入器(也称作应用程序 类装入器)负责从 CLASSPATH 环境变量指定的路径装入代码。默认情况下,这个类装入器是用户创建的任何类装入器的父类。这也是 ClassLoader.getSystemClassLoader() 方法返回的类装入器。 下表显示了三个类装入器各自的类路径: 命令行选项 解释 涉及的类装入器 -Xbootclasspath:<用 ; 或 : 分隔的目录和 zip/JAR 文件> 设置引导类和资源的搜索路径。 引导 -Xbootclasspath/a:<用 ; 或 : 分隔的目录和 zip/JAR 文件> 把路径添加到启动类路径的末尾。 引导 -Xbootclasspath/p:<用 ; 或 : 分隔的目录和 zip/JAR 文件> 把路径添加到启动类路径的前面。 引导 -Dibm.jvm.bootclasspath=<用 ; 或 : 分隔的目录和 zip/JAR 文件> 这个属性的值被用作额外的搜索路径,它被插到 -Xbootclasspath/p:-Xbootclasspath: 选项定义的值。 定义的值和启动类路径之间。启动类路径或者是默认值,或者是 引导 -Djava.ext.dirs=<用 ; 或 : 分隔的目录和 zip/JAR 文件> 指定扩展类和资源的搜索路径。 扩展 -cp or -classpath <用 ; 或 : 分隔的目录和 zip/JAR 文件> 设置应用程序类和资源的搜索路径。 系统 -Djava.class.path=<用 ; 或 : 分隔的目录和 zip/JAR 文件> 设置应用程序类和资源的搜索路径。 系统 三 JVM参数 在JVM中有一些参数提供了对classloader的调试信息。我们可以根据这些信息,对类装入过程中重要信息有所了解,利于我们分析,研究类的装入过 程。在实际工作中,我只遇到过Linkageerror这么一个有关的错误例子,为了分析这个错误,我使用了-verbose的JVM参数。在启动JVM 时加上这个参数,会是虚拟机在程序运行时,产生巨多类加载的信息,我们可以从中了解类是从哪里被加载到JVM里的。如果想要得到更详细的信息,可以设置 -verbose:class参数。至于其它更多的相关参数或者IBM提供的JVM中特有的参数,我没具体使用过就不多说了,可以去看本文第一篇参考文献 系列里的后续篇节。

四 相关异常 本来还想写一些相关例子,哈哈,想来想去,发现也没有第一篇参考文献系列里的例子好,所以就不写了,大家要看可以去IBM网站上看。http://www.ibm.com/developerworks/cn/java/j-dclp2.html 这里简单介绍一下相关的异常或错误,包括ClassNotFoundException,NoClassDefFoundErrorClassCastExceptionUnsatisfiedLinkError,ClassCircularityError,ClassFormatError,ExceptionInInitializer。前三个大家会经常碰到,后四个可能大家不太容易见到。

ExceptionInInitializer:如果初始化器突然完成,抛出一些异常 E,而且 E 的类不是 Error 或者它的某个子类,那么就会创建 ExceptionInInitializerError 类的一个新实例,并用 E 作为参数,用这个实例代替 E。如果 Java 虚拟机试图创建类 ExceptionInInitializerError 的新实例,但是因为出现 Out-Of-Memory-Error 而无法创建新实例,那么就抛出 OutOfMemoryError 对象作为代替。

ClassFormatError:负责指定所请求的编译类或接口的二进制数据形式有误。这个异常是在类装入的链接阶段 的校验过程中抛出。如果字节码发生了更改,例如主版本号或次版本号发生了更改,那么二进制数据的形式就会有误。例如,如果对字节码故意做了更改,或者在通 过网络传送类文件时现出了错误,那么就可能发生这个异常。修复这个问题的惟一方法就是获得字节码的正确副本,可能需要重新进行编译。

ClassCircularityError:类或接口由于是自己的超类或超接口而不能被装入。其实就是循环继承。A->B,B->A。

UnsatisfiedLinkError:就是说在寻找本地方法时,在类路径中没有找到需要的定义。

ClassCastException:太常见了,不说了。

ClassNotFoundException,NoClassDefFoundError:同样常见。这里说一下他们的区别。前者是被显式加载时,在类路径中找不到需要的类抛出的异常,后者是在隐式加载时,在类路径中找不到需要的类抛出的异常。显式,隐式的定义前面有介绍。

四 注意事项: 使用类装载器装载类的时候,类装载器会假设不以/结尾的路径指向的是JAR文件;而以/结尾的路径指向的是目录。 类装载器的双亲委托模式,每个类装载器的可视范围只包括本身和其双亲以及祖先。也就是说它看不到它的子装载器装载的类。 重载loadclass()方法,来实现自己的类装载器,以期采用非双亲委托模式的装载方式时,需要注意:程序中所有的类都必须在这个装载器的类路径中,包括java.lang.Object。

原创粉丝点击