1.类加载器简介
类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要原因之一。它使得 Java 类可以被动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 就出现了,最初是为了满足 Java Applet 的需要而开发出来的。Java Applet 需要从远程下载 Java 类文件到浏览器中并执行。现在类加载器在 Web 容器和 OSGi 中得到了广泛的使用。一般来说,Java 应用的开发人员不需要直接同类加载器进行交互。Java 虚拟机默认的行为就已经足够满足大多数情况的需求了。不过如果遇到了需要与类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就很容易花大量的时间去调试 ClassNotFoundException和 NoClassDefFoundError等异常。本文将详细介绍 Java 的类加载器,帮助读者深刻理解 Java 语言中的这个重要概念。下面首先介绍一些相关的基本概念。
2.类加载器基本概念
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:
- Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。
- 类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。
- 通过此实例的 newInstance()方法就可以创建出该类的一个对象。
- 实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
类加载器也是Java类,因为其它Java类的类加载器本身也要被类加载器加载,显然必须有第一个类加载器不是java类,这个就是BootStrap。BootStrap它是嵌套在Java虚拟机内核中的,jvm启动,这个类就会启动,它是由c++语言编写的。
Java虚拟机中的所有类加载器采用具有父子关系的树形结构进行组织,在实例化每个类加载器对象时,需要为其指定一个父级类加载器对象或者默认采用
系统类加载器(App ClassLoader)为其父级类加载。
图1:类加载器树状组织结构示意图
实例1:测试你所使用的JVM的ClassLoader
[java]view plaincopyprint?
- /*LoaderSample1.java*/
- publicclass LoaderSample1 {
- publicstaticvoid 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();
- }
- }
- }
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();
}
}
}
输出结果:
[java]view plaincopyprint?
- sun.misc.Launcher$AppClassLoader@40affc70
- -----------------
- sun.misc.Launcher$ExtClassLoader@61e63e3d
- null
- -----------------
- java.lang.Object:Classloader is null
- LoaderSample1:loader is sun.misc.Launcher$AppClassLoader@40affc70
sun.misc.Launcher$AppClassLoader@40affc70
-----------------
sun.misc.Launcher$ExtClassLoader@61e63e3d
null
-----------------
java.lang.Object:Classloader is null
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是由系统类装载器装载的
3.深入剖析
在Java中每个类都是由某个类加载器的实体来载入的,因此在Class类的实体中,都会有字段记录载入它的类加载器的实体(当为null时,其实是指Bootstrap ClassLoader)。 在java类加载器中除了引导类加载器(既Bootstrap ClassLoader),所有的类加载器都有一个父类加载器(因为他们本身自己就是java类)。而类的加载机制是遵循一种委托模式:当类加载器有加载类的需求时,会先请求其Parent加载(依次递归),如果在其父加载器树中都没有成功加载该类,则由当前类加载器加载。
Java的类加载器分为以下几种:
- Bootstrap ClassLoader:Bootstrap ClassLoader用C++实现,一切的开始,是所有类加载器的最终父加载器。负责将一些关键的Java类,如java.lang.Object和其他一些运行时代码先加载进内存中。
- ExtClassLoader:ExtClassLoader用java实现,是Launcher.java的内部类,编译后的名字为:Launcher$ExtClassLoader.class 。此类由Bootstrap ClassLoader加载,但由于Bootstrap ClassLoader已经脱离了java体系(c++),所以Launcher$ExtClassLoader.class的Parent(父加载器)被设置为null;它用于装载Java运行环境扩展包(jre/lib/ext)中的类,而且一旦建立其加载的路径将不再改变。
- AppClassLoader:AppClassLoader用java实现,也是是Launcher.java的内部类,编译后的名字为:Launcher$AppClassLoader.class 。AppClassLoader是当Bootstrap ClassLoader加载完ExtClassLoader后,再被Bootstrap ClassLoader加载。所以ExtClassLoader和AppClassLoader都是被Bootstrap ClassLoader加载,但AppClassLoader的Parent被设置为ExtClassLoader。可见Parent和由哪个类加载器来加载不一定是对应的。这个类装载器是我们经常使用的,可以调用ClassLoader.getSystemClassLoader() 来获得(实例1中使用了这个方法),如果程序中没有使用类装载器相关操作设定或者自定义新的类装载器,那么我们编写的所有java类都会由它来装载。而它的查找区域就是我们常常说到的Classpath,一旦建立其加载路径也不再改变。
- ClassLoader:ClassLoader一般我们自定义的ClassLoader从ClassLoader类继承而来。比如:URLClassloader是ClassLoader的一个子类,而URLClassloader也是ExtClassLoader和AppClassLoader的父类(注意不是父加载器)。
3.1.类加载器之间的父子关系为:
[java]view plaincopyprint?
- BootStrap -> ExtClassLoader -> AppClassLoader (即通常所说的System ClassLoader)
BootStrap -> ExtClassLoader -> AppClassLoader (即通常所说的System ClassLoader)
3.2.管辖范围依次是:
[java]view plaincopyprint?- BootStrap------>JRE/lib/rt.jar
- ExtClassLoader---------->JRE/lib/ext/*.jar
- AppClassLoader---------->CLASSPATH指定的所有jar或目录。
BootStrap------>JRE/lib/rt.jar
ExtClassLoader---------->JRE/lib/ext/*.jar
AppClassLoader---------->CLASSPATH指定的所有jar或目录。
3.3.类加载器的委托机制
当Java虚拟机要加载一个类时,到底该派哪个类加载器去加载呢?
- 首先是当前线程的类加载器去加载线程中的第一个类。
- 如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器来加载类B。
- 还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。
每个类加载器加载类时,又先委托给其上级类加载器。当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则抛出ClassNotFoundException,不是再去找发起者类加载器的儿子。因为没有getChlid方法,即使有,那么当有多个儿子,找哪一个呢?
3.4.当需要编写自己的类加载器时:
- 自定义的类加载器必须继承ClassLoader。
- 重写loadClass方法与findClass方法。loadClass中先调用父类的loadClass,然后调用findClass,通常情况下只覆盖findClass就可以。
- 重写defineClass方法。
- 注:自定义的类加载器通常用于解密自己写的已加密的class字节码,否则即使别人拥有该class文件也无法被系统的类加载器正常加载。
补充:(ps:2011-12-4)
参考:http://blog.csdn.net/lovingprince/article/details/4239491
4.parent delegation模型
从 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:
[java]view plaincopyprint?- ClassLoader loader1 = new MyClassLoader();
- //参数 loader1 为 parent
- ClassLoader loader2 = new ParentClassLoader(loader1);
ClassLoader loader1 = new MyClassLoader();
//参数 loader1 为 parent
ClassLoader loader2 = new ParentClassLoader(loader1);
那么parent delegation模型为什么更安全了? 因为在此模型下用户自定义的类装载器不可能装载应该由父亲装载器装载的可靠类,从而防止不可靠甚至恶意的代码代 替由父亲装载器装载的可靠代码。实际上,类装载器的编写者可以自由选择不用把请求委托给parent,但正如上所说,会带来安全的问题。
5.命名空间及其作用
每个类装载器有自己的命名空间,命名空间由所有以此装载器为创始类装载器的类组成。不同命名空间的两个类是不可见的,但只要得到类所对应的Class对象的reference,还是可以访问另一命名空间的类。例 2演示了一个命名空间的类如何使用另一命名空间的类。在例子中,LoaderSample2由系统类装载器装载,LoaderSample3由自定义的装 载器loader负责装载,两个类不在同一命名空间,但LoaderSample2得到了LoaderSample3所对应的Class对象的 reference,所以它可以访问LoaderSampl3中公共的成员(如age)。
例2不同命名空间的类的访问(代码未验证)
(1)创建java project:
URLClassLoaderTest1,在这个项目中创建LoaderSample3.java。然后将
uRLClassLoaderTest1/LoaderSample3.class文件打包成jar包:
jarLoaderSample3.jar。并将这个jar包放在d盘根目录下。
[java]view plaincopyprint?- package uRLClassLoaderTest1;
- /*sub/Loadersample3.java*/
- publicclass LoaderSample3 {
- publicint age = 30;
- //静态代码块,类被装在的时候自动运行。
- static {
- System.out.println("LoaderSample3 loaded");
- System.out.println(LoaderSample3.class.getClassLoader());//输出类装载器的类型
- }
- }
package uRLClassLoaderTest1;
/*sub/Loadersample3.java*/
public class LoaderSample3 {
public int age = 30;
//静态代码块,类被装在的时候自动运行。
static {
System.out.println("LoaderSample3 loaded");
System.out.println(LoaderSample3.class.getClassLoader());//输出类装载器的类型
}
}
(2)创建java project:
URLClassLoaderTest0,在创建LoaderSample2.java。
[java]view plaincopyprint?
- package uRLClassLoaderTest0;
- /*LoaderSample2.java*/
- import java.net.*;
- import java.lang.reflect.*;
- publicclass LoaderSample2 {
- publicstaticvoid 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();
- }
- }
- }
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();
}
}
}
运行结果:
[java]view plaincopyprint?
- sun.misc.Launcher$AppClassLoader@40affc70
- LoaderSample3 loaded
- java.net.URLClassLoader@544a5ab2
- age is 30
sun.misc.Launcher$AppClassLoader@40affc70
LoaderSample3 loaded
java.net.URLClassLoader@544a5ab2
age is 30
从运行结果中可以看出,在类LoaderSample2中可以创建处于另一命名空间的类LoaderSample3中的对象并可以访问其公共成员age。并且LoaderSample2是由系统类装载器AppClassLoader装载,而LoaderSample3则是由URLClassLoader装载。
6.运行时包(runtime package)
由 同一类装载器定义装载的属于相同包的类组成了运行时包,决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看的定义类装载器是否相 同。只有属于同一运行时包的类才能互相访问包可见的类和成员。这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。假设用户自 己定义了一个类java.lang.Yes,并用用户自定义的类装载器装载,由于java.lang.Yes和核心类库java.lang.*由不同的装 载器装载,它们属于不同的运行时包,所以java.lang.Yes不能访问核心类库java.lang中类的包可见的成员。
7.总结
在简单讨论了类装载器,parent delegation模型,命名空间,运行时包后,相信大家已经对它们的作用有了一定的了解。命名空间并没有完全禁止属于不同空间的类的互相访问,双亲委托模型加强了Java的安全,运行时包增加了对包可见成员的保护。