类加载器
类的加载过程中实现 “通过一个类的全限定名来获取描述该类的二进制字节流” 这个动作的代码被称为 “类加载器”。
类与类加载器
对于任意一个类,必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性。如果不注意类加载器的影响,可能导致 instanceof 关键字运算的结果令人迷惑。例如在下面的代码中,第一个 ClassLoaderTest 是由自定义的加载器加载的,而另一个 ClassLoaderTest 是由应用程序类加载器加载的,因此虽然它们的全类名相同,结果却返回 false。
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = 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(name);
}
}
};
Object obj = myLoader.loadClass("org.example.ClassLoaderTest").newInstance();
System.out.println(obj.getClass()); //class org.example.ClassLoaderTest
System.out.println(obj instanceof org.example.ClassLoaderTest); //false
}
}
双亲委派模型
上一节中有提到应用程序类加载器,那这是个什么呢?这就涉及到双亲委派模型了。在 JVM 的角度来看,存在两种不同的类加载器:一种是启动类加载器,由 C++ 实现,是虚拟机的一部分;另一种是其他所有的类加载器,都由 Java 实现,且全部继承自抽象类 java.lang.ClassLoader
。
事实上,自 JDK1.2 以来,Java 一直保持着三层类加载器、双亲委派的类加载架构,这种架构如图所示。
-
启动类加载器
这个类加载器负责加载存放在
<JAVA_HOME>\lib
目录下,并且能被 JVM 识别的类库。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,直接使用 null 代替即可。 -
扩展类加载器
这个类加载器负责加载
<JAVA_HOME>\lib\ext
目录下,或者被 java.ext.dirs 系统变量所指定的路径中所有的类库。扩展类加载器直接由 Java 代码实现,因此可以在程序中直接使用它来加载 Class 文件。 -
应用程序类加载器
应用程序类加载器也被称为系统类加载器,负责加载用户类路径上所有的类库,开发者同样可以直接在代码中使用这个类加载器。
接下来就轮到本节的重点内容 —— 双亲委派模型闪亮登场啦!双亲委派模型的流程如下:
- 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
- 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器
loadClass()
方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器BootstrapClassLoader
中。 - 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的
findClass()
方法来加载类)。 - 如果子类加载器也无法加载这个类,那么它会抛出一个
ClassNotFoundException
异常。
双亲委派模型显而易见的好处是 Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系,例如类 java.lang.Object
,它存在于 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此如果用户自定义 Object 类,那么在应用程序加载器试图加载这个类的时候,会发现父类加载器已经加载过了,从而放弃加载。
双亲委派模型对于 Java 程序的稳定运行极为重要,但实现却异常简单:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果 c 为 null,则说明该类没有被加载过
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
//用户可通过覆写该方法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
破坏双亲委派模型
从上面双亲委派模型的执行流程可以看出,如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass()
方法,无法被父类加载器加载的类最终会通过这个方法被加载;如果想打破双亲委派模型则需要重写 loadClass()
方法,因为 loadClass() 就是实现双亲委派逻辑的地方。
Tomcat 是一个典型的打破双亲委派模型的例子。我们知道一个 Tomcat 可以部署多个 Web 应用程序,假设现在有两个 Web 应用程序,它们都有一个叫做 User 的类,并且拥有相同的全类名,为了保证两者不冲突,Tomcat 给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了 loadClass 方法,优先加载当前应用目录下的类,只有当前找不到了才一层一层往上找。