标题叫做正传,就是说这一部分主要讲的是Java中类加载器的正统机制和特点。有正传就意味着有对应的外传,外传部分会主要介绍下对正统的“破坏”行为。
Java类加载器用于加载类的字节码并实例化为Class对象。正统的做法下,它具有以下两个特征。
- 树形层次结构
- 父辈委派加载
树形层次结构
由下图可以直观的看出,各个class loader之间形成了树状的一个层次结构,除了根节点的class loader,其它每一个class loader都有一个父加载器,有点类似java的类继承体系一样。
树中顶部的三个是由jvm默认提供的三个类加载器。它们各自有不同的职责:
Bootstrap Class Loader
该加载器负责加载java核心库
Ext Class Loader
java在其ext目录中放置了扩展库,该加载器就是负责加载这里面的类的
System Class Loader
这个加载器就是用于加载用户编写的class的。
为什么要用不同的类加载器分别加载不同类型的类呢?主要原因是为了保证java的核心类库(以java命名的package下的class)可以被正确的加载,并且只加载一次,避免运行时出现多个核心类的Class实例。具体怎么做到这一点的,就要看类加载器的另一个特点【父辈委派加载】了。
父辈委派加载
父辈委派加载模型算是我个人对这个机制英文名称的一个翻译,其英文原文是Parents Delegation Model,网上看有翻译为“双亲委派模型”,个人觉得这里的“双亲”容易造成理解障碍,实际上每个类加载器只有一个单亲而已。
这里首先说一下jvm中对两个类是否相同的判定标准:每一个类都需要由加载它的类加载器和类本身一起确定其在jvm中的唯一性。这里类加载器起到了一个命名空间的作用,因此,即使同一个类的class文件,如果被不同的类加载器加载,那么在jvm中它们也是不相同的。这时如果对其中一个类加载器加载的类的实例赋值给另一个类加载器加载的同名类,则会抛出ClassCastException,如下代码示例及运行结果所示。
package com.leo.base.javas.jvm;
import java.io.IOException;
import java.io.InputStream;
public class ClassIdentityTest {
public static void main(String[] args) throws Exception {
ClassLoader loader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf("." ) + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
};
ClassIdentityTest obj = (ClassIdentityTest) loader.loadClass("com.leo.base.javas.jvm.ClassIdentityTest").newInstance();
}
}
运行结果
Exception in thread "main" java.lang.ClassCastException: com.leo.base.javas.jvm.ClassIdentityTest cannot be cast to com.leo.base.javas.jvm.ClassIdentityTest
at com.leo.base.javas.jvm.ClassIdentityTest.main(ClassIdentityTest.java:29)
父辈委派加载模型从jdk1.2引入。它并不是一个强制性的要求机制,只是java推荐给开发者的一个方式,因此后面的外传中我们可以看到有许多java的应用或框架都会打破这一模型,实现自己特殊用途的类加载方式(例如tomcat、OSGi等等)。
不过另一方面,对于java核心库的加载,还是逃脱不了这个机制。例如,如果自己写一个java.lang.Object类,然后自己写一个类加载器强行加载它,则会抛出java.lang.SecurityException:Prohibited package name:java.lang的异常,也就是说jvm从根本上保证了java基础体系在运行期的正确性。
具体的,父辈委派加载的具体过程是:
- 如果一个class loader接收到了对一个class的加载请求,那么它首先检查这个类是否被加载过,如果加载过,则直接由它自己完成后续处理
- 如果class没有被加载过,那么loader将这个请求委派给它的父加载器加载。此时,如果父加载器为null,则使用Bootstrap Class Loader来尝试加载。如果不为null,则说明是父加载器是System或Ext的loader。
- 如果父加载器加载不成功,则有它本身完成加载,或加载失败抛出ClassNotFoundException。
这一过程在jdk1.8中java.lang.ClassLoader抽象类的实现代码如下
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}