Java的类装载器和命名空间
摘要
Java的类装载器是Java动态性的核心,本文将向大家简要介绍Java的类装载器,及相关的parent delegation模型,命名空间,运行时包等概念,同时讨论一些在学习中容易混淆的问题。
类装载器的功能及分类
顾名思义,类装载器是用来把类(class)装载进JVM的。
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
· 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自java.lang.ClassLoader。(负责加载java_home/lib目录下的核心类或- Xbootclasspath指定目录下的类 )
· 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。(负责加载java_home/lib/ext目录下的扩展类或 -Djava.ext.dirs 指定目录下的类)
· 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。(负责加载-classpath/-Djava.class.path所指的目录下的类。)
除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。Java提供了抽象类ClassLoader,所有用户自定义类装载器都实例化自ClassLoader的子类。
系统类加载器(System Class Loader)是一个特殊的用户自定义类装载器,由JVM的实现者提供,在编程者不特别指定装载器的情况下默认装载用户类。系统类装载器可以通过 ClassLoader.getSystemClassLoader() 方法得到。
例1,测试你所使用的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.5)的运行结果:
C:\java>java LoaderSample1
sun.misc.Launcher$AppClassLoader@82ba41
sun.misc.Launcher$ExtClassLoader@923e30
null
java.lang.Object's loader is null
LoaderSample1's loader is sun.misc.Launcher$AppClassLoader@82ba41
第一行表示,系统类装载器实例化自类sun.misc.Launcher$AppClassLoader
第二行表示,系统类装载器的parent实例化自类sun.misc.Launcher$ExtClassLoader
第三行表示,系统类装载器parent的parent为bootstrap
第四行表示,核心类java.lang.Object是由bootstrap装载的
第五行表示,用户类LoaderSample1是由系统类装载器装载的
parent delegation模型
从1.2版本开始,Java引入了双亲委托模型,从而更好的保证Java平台的安全。在此模型下,当一个装载器被请求装载某个类时,它首先委托自己的parent去装载,若parent能装载,则返回这个类所对应的Class对象,若parent不能装载,则由parent的请求者去装载。
如图1所示,类加载器B2的parent为类加载器B,类加载器B的parent为系统类加载器。假设 类加载器B2被要求装载类MyClass,在parent delegation模型下,类加载器B2首先请求类加载器B代为装载,类加载器B再请求系统类装载器去装载MyClass,系统类装载器也会继续请求它的Parent扩展类加载器去装载MyClass,以此类推直到引导类装载器。若引导类装载器能成功装载,则将MyClass所对应的Class对象的reference逐层返回到类加载器B2,若引导类装载器不能成功装载,下层的扩展类装载器将尝试装载,并以此类推直到类装载器B2如果也不能成功装载,则装载失败。
若有一个能成功装载,实际装载的类装载器被称为定义类装载器,所有能成功返回Class对象的装载器(包括定义类装载器)被称为初始类装载器。如图1所示,假设类加载器B实际装载了MyClass,则类加载器B为MyClass的定义类装载器,类加载器B2和类加载器B为 MyClass的初始类装载器。
图1 parent delegation模型
需要指出的是,Class Loader是对象,它的父子关系和类的父子关系没有任何关系。一对父子loader可能实例化自同一个 Class,也可能不是,甚至父loader实例化自子类,子loader实例化自父类。假设MyClassLoader继承自 ParentClassLoader,我们可以有如下父子loader:
ClassLoader 类加载器B = new MyClassLoader();
//参数 类加载器B 为 parent
ClassLoader 类加载器B2 = new ParentClassLoader(类加载器B);
那么parent delegation模型为什么更安全了?因为在此模型下用户自定义的类装载器不可能装载应该由父亲装载器装载的可靠类,从而防止不可靠甚至恶意的代码代替由父亲装载器装载的可靠代码。实际上,类装载器的编写者可以自由选择不用把请求委托给parent,但正如上所说,会带来安全的问题。
命名空间及其作用
每个类装载器有自己的命名空间,命名空间由所有以此装载器为初始类装载器的类组成。不同命名空间的两个类是不可见的,但只要得到类所对应的Class对象的reference,还是可以访问另一命名空间的类。
例2演示了一个命名空间的类如何使用另一命名空间的类。在例子中,LoaderSample2由系统类装载器装载,LoaderSample3由自定义的装载器loader负责装载,两个类不在同一命名空间,但LoaderSample2得到了 LoaderSample3所对应的Class对象的reference,所以它可以访问LoaderSampl3中公共的成员(如age)。
例2 不同命名空间的类的访问
/*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();
}
}
}
/*sub/Loadersample3.java*/
public class LoaderSample3 {
static {
System.out.println("LoaderSample3 loaded");
}
public int age = 30;
}
编译:
javac LoaderSample2.java;
javac sub/LoaderSample3.java
运行:java LoaderSample2
LoaderSample3 loaded
age is 30
从运行结果中可以看出,在类LoaderSample2中可以创建处于另一命名空间的类LoaderSample3中的对象并可以访问其公共成员age。
运行时包(runtime package)
由同一类装载器定义装载的属于相同包的类组成了运行时包,决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看类装载器是否相同。只有属于同一运行时包的类才能互相访问包可见的类和成员。这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。假设用户自己定义了一个类java.lang.Yes,并用用户自定义的类装载器装载,由于java.lang.Yes和核心类库java.lang.*由不同的装载器装载,它们属于不同的运行时包,所以java.lang.Yes不能访问核心类库java.lang中类的包可见的成员。
总结
在简单讨论了类装载器,parent delegation模型,命名空间,运行时包后,相信大家已经对它们的作用有了一定的了解。命名空间并没有完全禁止属于不同空间的类的互相访问,双亲委托模型加强了Java的安全,运行时包增加了对包可见成员的保护。