来源: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的文章中截过来的,所以是英文,不好意思了。
![类装入的阶段](https://i-blog.csdnimg.cn/blog_migrate/5275155f74cf6abaa94d3d5707d9fc1f.png)
装载就是把二进制形式的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
- /*LoaderSample1.java*/
- public class LoaderSample1 {
- public static void main(String[] args) {
- Class c;
- ClassLoader cl;
- cl = ClassLoader.getSystemClassLoader();
- System.out.println(cl);
- while (cl != null) {
- cl = cl.getParent();
- System.out.println(cl);
- }
- try {
- c = Class.forName("java.lang.Object");
- cl = c.getClassLoader();
- System.out.println("java.lang.Object's loader is " + cl);
- c = Class.forName("LoaderSample1");
- cl = c.getClassLoader();
- System.out.println("LoaderSample1's loader is " + cl);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
在我的机器上(Sun Java 1.4.2)的运行结果
sun.misc.Launcher$AppClassLoader@1a0c10f sun.misc.Launcher$ExtClassLoader@e2eec8 null java.lang.Object's loader is null LoaderSample1'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 代码
- /*LoaderSample2.java*/
- import java.net.*;
- import java.lang.reflect.*;
- public class LoaderSample2 {
- public static void main(String[] args) {
- try {
- String path = System.getProperty("user.dir");
- URL[] us = {new URL("file://" + path + "/sub/")};
- ClassLoader loader = new URLClassLoader(us);
- Class c = loader.loadClass("LoaderSample3");
- Object o = c.newInstance();
- Field f = c.getField("age");
- int age = f.getInt(o);
- System.out.println("age is " + age);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- /*Loadersample3.java*/
- public class LoaderSample3 {
- static {
- System.out.println("LoaderSample3 loaded");
- }
- public int age = 30;
- }
LoaderSample3 loaded age is 30
从运行结果中可以看出,在类LoaderSample2中可以创建处于另一命名空间的类LoaderSample3中的对象并可以访问其公共成员age。
二 classloader实现机制 类装载器采用双亲委派模型。在建立用户自定义的类装载器时可以指定其双亲。如果没有指定,则默认的是把系统类装载器作为其双亲。如果向用户自定义的装载器的构造方法里传递null,则引导装载器就是双亲。 classloader依次从缓存,双亲,自己来寻找类。classloader首先判断要求它装入的类是否与过去装入的类相同。如果相同,就返回上次返 回的类(即保存在缓存中的类)。如果不是,就把装入类的机会交给父类。这两步递归地以深度优先的方式重复。如果父类返回 null(或抛出 ClassNotFoundException
),那么类装入器会在自己的路径中寻找类的源。 因为父类类装入器总是先得到装入类的机会,所以classloader装入的类最靠近根。这意味着所有核心引导类都是由引导装入器装入的,这就保证装入了类(例如 java.lang.Object
)的正确版本。这也可以让类装入器看到自己或父类或祖先装入的类,但是不能看到子女装入的类。
![类装入器委托模型](https://i-blog.csdnimg.cn/blog_migrate/d7c3da06fa01b4e4718c7cde057f91ae.png)
-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 文件> | 设置应用程序类和资源的搜索路径。 | 系统 |
四 相关异常 本来还想写一些相关例子,哈哈,想来想去,发现也没有第一篇参考文献系列里的例子好,所以就不写了,大家要看可以去IBM网站上看。http://www.ibm.com/developerworks/cn/java/j-dclp2.html 这里简单介绍一下相关的异常或错误,包括
ClassNotFoundException,
NoClassDefFoundError
,ClassCastException
,UnsatisfiedLinkError,
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。