Java类加载器

三、双亲委派模型

从Java虚拟机的角度来讲, 只存在两种不同的类加载器: 一种是启动类加载器(Bootstrap ClassLoader), 这个类加载器使用C++语言实现(这里只限于HotSpot,像MRP, Maxine等虚拟机, 整个虚拟机本身都是由Java编写的, 自然Bootstrap ClassLoader 也是由Java语言而不是C++实现的, 退一步讲, 除了HotSpot以外的其他两个高性能虚拟机JRockit和J9都有一个代表Bootstrap ClassLoader的java类存在, 但是关键方法的实现仍然是使用JNI回调到C, 不是C++的实现上, 这个Bootstrap ClassLoader的实例也无法被用户获取到.), 是虚拟机自身的一部分;另一种就是所有其他的类加载器, 这些类加载器都由java语言实现,独立于虚拟机外部, 并且全都继承自抽象类java.lang.ClassLoader.

从java开发人员的角度来看, 类加载器还可以划分得更细致一些, 绝大多数java程序都会使用到以下3种系统提供的类加载器.

1) 启动类加载器(Bootstrap ClassLoader): 这个类加载器负责将存放在JAVA_HOME\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别, 如rt.jar 名字不符合的类库即使放在lib目录中也不会被加载) 类库加载到虚拟机内存中. 启动类加载器无法被java程序直接引用, 用户在编写自定义类加载器时, 如果需要把加载请求委派给引导类加载器, 那直接使用null代替即可. 下面的代码就是java.lang.ClassLoader.getClassLoader()方法的代码片段:

public ClassLoader getClassLoader() {

ClassLoader cl = getClassLoader();

if (cl == null)

return null;

SecurityManager sm = System.getSecurityManager();

if (sm != null) {

ClassLoader ccl = ClassLoader.getCallerClassLoader();

if (ccl != null && ccl != cl && !cl.isAncestor(ccl)) {

sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);

}

}

return cl;

}

2) 扩展类加载器(Extension ClassLoader) : 这个加载器由sun.misc.Launcher$ExtClassLoader实现, 它负责加载JAVA_HOME/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库, 开发者可以直接使用扩展类加载器.

3) 应用程序类加载器(Application ClassLoader) :  这个类加载器由sun.misc.Launcher$AppClassLoader实现. 由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值, 所以一般也称它为系统类加载器. 它负责加载用户类路径(classpath)上所指定的类库, 开发者可以直接使用这个类加载器, 如果应用程序中没有自定义过自己的类加载器, 一般情况下这个就是程序中默认的类加载器.

我们的应用程序都是由这3种类加载器互相配合进行加载的, 如果有必要, 还可以加入自己定义的类加载器. 这些类加载器之间的关系是

启动类加载器

扩展类加载器

应用程序类加载器

自定义类加载器1, 自定义类加载器2 ....

上面的类是下面的类的父类. 这种关系,称为类加载器的双亲委派模型(Parents Delegation Model). 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器. 这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现, 而是都使用组合(Composition)关系来复用父加载器的代码.

类加载器的双亲委派模型在JDK1.2期间被引入并被广泛应用于之后几乎所有的java程序中,但它并非不是一个强制性的约束模型,而是java设计者推荐给开发者的一种类加载器实现方式.

双亲委派模型的工作过程是 : 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成, 每一个层次的类加载器都是如此, 因此所有的加载请求最都应该传送到顶层的启动类加载器中, 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围内没有找到所需的类)时, 子加载器才会尝试自己去加载.

使用双亲委派模型来组织类加载器之间的关系, 有一个显而易见的好处就是java类随着它的类加载器一起具备了一种带有优先级的层次关系. 例如类Java.lang.Object, 它存放在rt.jar之中, 无论哪一个类加载器要加载这个类, 最终都是委派给处于模型最顶端的启动类加载器进行加载, 由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的classpath中,那系统中将会出现多个不同的Object类, java类型体系中最基础的行为也就无法保证, 应用程序也将会变得一片混乱. 如果我们尝试编写一个与rt.jar类库中已有类重名的java类, 将会发现可以正常编译,但永远无法被加载运行. 即使自定义了自己的类加载器, 强行用defineClass()方法去加载一个以"java.lang"开头的类也不会成功. 如果尝试这样做的话, 将会收到一个由虚拟机自己抛出异常"java.lang.SecurityException:Prohibited package name:java.lang"

双亲委派模型对于保证java程序的稳定动作很重要, 但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中, 如下所示, 逻辑清晰, 先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法, 若父加载器为空则默认使用启动类加载器作为父加载器. 如果父类加载失败,抛出ClassNotFoundException异常后, 再调用自己的findClass()方法进行加载.

代码如下:

protected synchronized Class<?> loadClass(String name, boolean resolve)

throws ClassNotFoundException {

// 首先, 检查请求的类是否已经被加载过了

Class c = findLoadedClass(name);

if (c == null) {

try {

if (parent != null) {

c = parent.loadClass(name, false);

} else {

c = findBootstrapClassOrNull(name);

}

} catch (ClassNotFoundException e) {

// 如果父类加载器抛出异常

// 说明父类加载器无法完成加载请求

}

if (c == null) {

// 在父类加载器无法加载的时候, 再调用本身的findClass方法

c = findClas(name);

}

if (resolve) {

resolveClass(c);

}

return c;

}

}