本文共 8251 字,大约阅读时间需要 27 分钟。
类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要原因之一。它使得 Java 类可以被动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 就出现了,最初是为了满足 Java Applet 的需要而开发出来的。Java Applet 需要从远程下载 Java 类文件到浏览器中并执行。现在类加载器在 Web 容器和 OSGi 中得到了广泛的使用。一般来说,Java 应用的开发人员不需要直接同类加载器进行交互。Java 虚拟机默认的行为就已经足够满足大多数情况的需求了。不过如果遇到了需要与类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就很容易花大量的时间去调试 ClassNotFoundException和 NoClassDefFoundError等异常。本文将详细介绍 Java 的类加载器,帮助读者深刻理解 Java 语言中的这个重要概念。下面首先介绍一些相关的基本概念。
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
类加载器也是Java类,因为其它Java类的类加载器本身也要被类加载器加载,显然必须有第一个类加载器不是java类,这个就是BootStrap。BootStrap它是嵌套在Java虚拟机内核中的,jvm启动,这个类就会启动,它是由c++语言编写的。Java虚拟机中的所有类加载器采用具有父子关系的树形结构进行组织,在实例化每个类加载器对象时,需要为其指定一个父级类加载器对象或者默认采用系统类加载器(App ClassLoader)为其父级类加载。
/*LoaderSample1.java*/ public class LoaderSample1 { public static void main(String[] args) { Class c; ClassLoader cl; cl = ClassLoader.getSystemClassLoader();// 系统类装载器实例化 System.out.println(cl);//sun.misc.Launcher$AppClassLoader@40affc70 System.out.println("-----------------"); while (cl != null) { cl = cl.getParent();// parent实例化 System.out.println(cl);//sun.misc.Launcher$ExtClassLoader@61e63e3d和null } System.out.println("-----------------"); try { c = Class.forName("java.lang.Object"); cl = c.getClassLoader();//获取核心类java.lang.Object的类加载器 System.out.println(c.getName()+":Classloader is " + cl); c = Class.forName("LoaderSample1"); cl = c.getClassLoader();//获取用户类LoaderSample1的类加载器 System.out.println(c.getName()+":loader is " + cl); } catch (Exception e) { e.printStackTrace(); } } }
1 sun.misc.Launcher$AppClassLoader@40affc70 2 ----------------- 3 sun.misc.Launcher$ExtClassLoader@61e63e3d 4 null 5 ----------------- 6 java.lang.Object:Classloader is null 7 LoaderSample1:loader is sun.misc.Launcher$AppClassLoader@40affc70
第1行表示,系统类装载器实例化自类sun.misc.Launcher$AppClassLoader
第3行表示,系统类装载器的parent实例化自类sun.misc.Launcher$ExtClassLoader 第4行表示,系统类装载器parent的parent为bootstrap 第6行表示,核心类java.lang.Object是由bootstrap装载的 第7行表示,用户类LoaderSample1是由系统类装载器装载的这里的null表示的就是bootstrap类装载器。这个在第三节会解释。
在Java中每个类都是由某个类加载器的实体来载入的,因此在Class类的实体中,都会有字段记录载入它的类加载器的实体(当为null时,其实是指Bootstrap ClassLoader)。 在java类加载器中除了引导类加载器(既Bootstrap ClassLoader),所有的类加载器都有一个父类加载器(因为他们本身自己就是java类)。而类的加载机制是遵循一种委托模式:当类加载器有加载类的需求时,会先请求其Parent加载(依次递归),如果在其父加载器树中都没有成功加载该类,则由当前类加载器加载。
Java的类加载器分为以下几种:
BootStrap -> ExtClassLoader -> AppClassLoader (即通常所说的System ClassLoader)
BootStrap------>JRE/lib/rt.jar
ExtClassLoader---------->JRE/lib/ext/*.jar AppClassLoader---------->CLASSPATH指定的所有jar或目录。当Java虚拟机要加载一个类时,到底该派哪个类加载器去加载呢?
每个类加载器加载类时,又先委托给其上级类加载器。当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则抛出ClassNotFoundException,不是再去找发起者类加载器的儿子。因为没有getChlid方法,即使有,那么当有多个儿子,找哪一个呢?
补充:(ps:2011-12-4)
参考:
从 1.2版本开始,Java引入了双亲委托模型,从而更好的保证Java平台的安全。在此模型下,当一个装载器被请求装载某个类时,它首先委托自己的parent去装载,若parent能装载,则返回这个类所对应的Class对象,若parent不能装载,则由parent的请求者去装载。
如 图2所示,loader2的parent为loader1,loader1的parent为system class loader。假设loader2被要求装载类MyClass,在parent delegation模型下,loader2首先请求loader1代为装载,loader1再请求系统类装载器去装载MyClass。若系统装载器能成功装载,则将MyClass所对应的Class对象的reference返回给loader1,loader1再将reference返回给 loader2,从而成功将类MyClass装载进虚拟机。若系统类装载器不能装载MyClass,loader1会尝试装载MyClass,若 loader1也不能成功装载,loader2会尝试装载。若所有的parent及loader2本身都不能装载,则装载失败。
若有一个能成功装载,实际装载的类装载器被称为定义类装载器,所有能成功返回Class对象的装载器(包括定义类装载器)被称为初始类装载器。如图1所示,假设loader1实际装载了MyClass,则loader1为MyClass的定义类装载器,loader2和loader1为MyClass的初始类装载器。
图 2 parent delegation模型
需要指出的是,Class Loader是对象,它的父子关系和类的父子关系没有任何关系。一对父子loader可能实例化自同一个Class,也可能不是,甚至父loader实例化自子类,子loader实例化自父类。假设MyClassLoader继承自ParentClassLoader,我们可以有如下父子loader:
ClassLoader loader1 = new MyClassLoader();//父loader实例化自子类,MyClassLoader是ParentClassLoader的子类 //参数 loader1 为 parent ClassLoader loader2 = new ParentClassLoader(loader1);
那么parent delegation模型为什么更安全了?因为在此模型下用户自定义的类装载器不可能装载应该由父亲装载器装载的可靠类,从而防止不可靠甚至恶意的代码代 替由父亲装载器装载的可靠代码。实际上,类装载器的编写者可以自由选择不用把请求委托给parent,但正如上所说,会带来安全的问题。
每个类装载器有自己的命名空间,命名空间由所有以此装载器为创始类装载器的类组成。不同命名空间的两个类是不可见的,但只要得到类所对应的Class对象的reference,还是可以访问另一命名空间的类。例 2演示了一个命名空间的类如何使用另一命名空间的类。在例子中,LoaderSample2由系统类装载器装载,LoaderSample3由自定义的装 载器loader负责装载,两个类不在同一命名空间,但LoaderSample2得到了LoaderSample3所对应的Class对象的 reference,所以它可以访问LoaderSampl3中公共的成员(如age)。
package uRLClassLoaderTest1; /*sub/Loadersample3.java*/ public class LoaderSample3 { public int age = 30; //静态代码块,类被装在的时候自动运行。 static { System.out.println("LoaderSample3 loaded"); System.out.println(LoaderSample3.class.getClassLoader());//输出类装载器的类型 } }
package uRLClassLoaderTest0; /*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:d:/jarLoaderSample3.jar")}; ClassLoader loader = new URLClassLoader(us); Class c = loader.loadClass("uRLClassLoaderTest1.LoaderSample3"); System.out.println(LoaderSample2.class.getClassLoader());//输出类装载器的类型 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(); } } }
sun.misc.Launcher$AppClassLoader@40affc70 LoaderSample3 loaded java.net.URLClassLoader@544a5ab2 age is 30
从运行结果中可以看出,在类LoaderSample2中可以创建处于另一命名空间的类LoaderSample3中的对象并可以访问其公共成员age。并且LoaderSample2是由系统类装载器AppClassLoader装载,而LoaderSample3则是由URLClassLoader装载。
由 同一类装载器定义装载的属于相同包的类组成了运行时包,决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看的定义类装载器是否相 同。只有属于同一运行时包的类才能互相访问包可见的类和成员。这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。假设用户自 己定义了一个类java.lang.Yes,并用用户自定义的类装载器装载,由于java.lang.Yes和核心类库java.lang.*由不同的装 载器装载,它们属于不同的运行时包,所以java.lang.Yes不能访问核心类库java.lang中类的包可见的成员。
在简单讨论了类装载器,parent delegation模型,命名空间,运行时包后,相信大家已经对它们的作用有了一定的了解。命名空间并没有完全禁止属于不同空间的类的互相访问,双亲委托模型加强了Java的安全,运行时包增加了对包可见成员的保护。
本文转自xwdreamer博客园博客,原文链接:http://www.cnblogs.com/xwdreamer/archive/2011/12/01/2296919.html,如需转载请自行联系原作者