类加载器与双亲委派机制
目录
一、类与类加载器
类加载器用于实现类的加载,加载器会把载入内存中的类生成一个java.lang.Class实例对象。对于任意一个类, 都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。也就是说:比较两个类是否“相等”, 只有在这两个类是由同一个类加载器加载的前提下才有意义, 否则, 即使这两个类来源于同一个Class文件, 被同一个Java虚拟机加载, 只要加载它们的类加载器不同, 那这两个类就必定不相等。下面是类加载器对instanceof关键字运算的结果的影响:
package com.me.jvm;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
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(name);
}
}
};
Object obj = loader.loadClass("com.me.jvm.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof ClassLoaderTest);
System.out.println(obj.getClass().getClassLoader());
System.out.println(ClassLoaderTest.class.getClassLoader());
/**
* 打印结果:
* class com.me.jvm.ClassLoaderTest
* false
* com.me.jvm.ClassLoaderTest$1@74a14482
* sun.misc.Launcher$AppClassLoader@18b4aac2
*/
}
}
上面的示例显示:在自定义的classLoader去加载了一个名为“com.me.jvm.ClassLoaderTest”的类, 并实例化了这个类的对象。前两行输出结果中, 从第一行可以看到这个对象确实是类com.me.jvm.ClassLoaderTest实例化出来的, 但在第二行的输出中却发现这个对象与类com.me.jvm.ClassLoaderTest做所属类型检查的时候返回了false。 这是因为Java虚拟机中同时存在了两个ClassLoaderTest类, 一个是由虚拟机的应用程序类加载器所加载的(参考第四行结果), 另外一个是由我们自定义的类加载器加载的(参考第三行结果), 虽然它们都来自同一个Class文件, 但在Java虚拟机中仍然是两个互相独立的类。
二、三层类加载器
本节内容将针对JDK 8及之前版本的Java来介绍什么是三层类加载器。正如我们通常认为的那样,绝大多数Java程序都会使用到以下3个系统提供的类加载器来进行加载。分别是:启动类加载器、扩展类加载器、应用程序类加载器。贴出如下代码来查看各层加载器及其父类加载器:
/*示例1:输出加载器的父类加载器*/
Object obj = loader.loadClass("com.me.jvm.ClassLoaderTest").newInstance();//loader为第一节中自定义加载器
//1.1
System.out.println(obj.getClass().getClassLoader());//com.me.jvm.ClassLoaderTest$1@74a14482
//1.2
System.out.println(ClassLoaderTest.class.getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2
//1.3
System.out.println(ClassLoaderTest.class.getClassLoader().getParent());//sun.misc.Launcher$ExtClassLoader@14ae5a5
//1.4
System.out.println(ClassLoaderTest.class.getClassLoader().getParent().getParent());//null
/*示例2:输出各个类的加载器*/
//2.1
System.out.println(ClassLoaderTest.class.getClassLoader());//sun.misc.Launcher$AppClassLoader
//2.2
System.out.println(ClassLoaderTest.class.getClassLoader().getClass().getClassLoader());//null
//2.3
System.out.println(com.sun.nio.zipfs.ZipPath.class.getClassLoader());//sun.misc.Launcher$ExtClassLoader@14ae5a5
//2.4
System.out.println(com.sun.nio.zipfs.ZipPath.class.getClassLoader().getClass().getClassLoader());//null
从示例一中可以看出,其中三层加载器及自定义加载器是相互补充依赖,并不是继承关系,查看源码的话可以看出加载器ClassLoader类里有一个final修饰的ClassLoader类型的parent属性。加载器的依赖顺序是:自定义加载器的-->应用程序类加载器(AppClassLoader)-->扩展类加载器(ExtClassLoader)-->启动类加载器(null)。从示例二中可以看出,各层加载器加载的类是不一样的。下面是三层加载器的说明 :
- ·启动类加载器(Bootstrap Class Loader)
这个类加载器负责加载存放在<JAVA_HOME>\lib目录, 或者被-Xbootclasspath参数所指定的路径中存放的, 而且是Java虚拟机能够识别的(按照文件名识别, 如rt.jar、 tools.jar, 名字不符合的类库即使放在lib目录中也不会被加载) 类库加载到虚拟机的内存中。 启动类加载器无法被Java程序直接引用, 用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理, 那直接使用null代替即可( null值来代表引导类加载器的约定规则,可参考上面示例2.2或2.4)。
- ·扩展类加载器(Extension Class Loader) :
这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。 它负责加载<JAVA_HOME>\lib\ext目录中, 或者被java.ext.dirs系统变量所指定的路径中所有的类库。 根据“扩展类加载器”这个名称, 就可以推断出这是一种Java系统类库的扩展机制, JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能, 在JDK9之后, 这种扩展机制被模块化带来的天然的扩展能力所取代。 由于扩展类加载器是由Java代码实现的, 开发者可以直接在程序中使用扩展类加载器来加载Class文件(可参考上面示例2.3)。
- ·应用程序类加载器(Application Class Loader)/系统类加载器 :
这个类加载器由sun.misc.Launcher$AppClassLoader来实现。 由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值, 所以有些场合中也称它为“系统类加载器”。 它负责加载用户类路径(ClassPath) 上所有的类库, 开发者同样可以直接在代码中使用这个类加载器。 如果应用程序中没有自定义过自己的类加载器, 一般情况下这个就是程序中默认的类加载器。(可参考上面示例2.1)。
三、双亲委派机制
双亲委派模型的工作过程是: 如果一个类加载器收到了类加载的请求, 它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成, 每一个层次的类加载器都是如此, 因此所有的
加载请求最终都应该传送到最顶层的启动类加载器中, 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类) 时, 子加载器才会尝试自己去完成加载。
使用双亲委派模型来组织类加载器之间的关系, 一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。 例如类java.lang.Object, 它存放在rt.jar之中, 无论哪一个类加载器要加载这个类, 最终都是委派给处于模型最顶端的启动类加载器进行加载, 因此Object类在程序的各种类加载器环境中都能够保证是同一个类。 反之, 如果没有使用双亲委派模型, 都由各个类加载器自行去加载的话, 如果用户自己也编写了一个名为java.lang.Object的类, 并放在程序的ClassPath中, 那系统中就会出现多个不同的Object类, Java类型体系中最基础的行为也就无从保证, 应用程序将会变得一片混乱。双亲委派模型对于保证Java程序的稳定运作极为重要, 但它的实现却异常简单,全部集中在java.lang.ClassLoader的loadClass()方法之中, 如代码如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1、查看是否已经加载过此类
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,说明父类加载器无法加载
}
if (c == null) {
// 假如父类加载此类失败,调用自身findClass方法再进行类加载
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;
}
}
这段代码的逻辑: 先检查请求加载的类型是否已经被加载过, 若没有则调用父加载器的loadClass()方法, 若父加载器为空则默认使用启动类加载器作为父加载器。 假如父类加载器加载失败,抛出ClassNotFoundException异常的话, 才调用自己的findClass()方法尝试进行加载。
下面是类加载器加载流程图:
四、总结
本文所用jdk版本为java1.8.0_201,以下是总结:
- 自定义加载器可以通过重写ClassLoader的loadClass方法来实现。
- class都是通过classloader来装载的。
- 只有当你使用该class的时候才会去装载。
- 同一个加载器只会装载相同的class一次;同一个Java虚拟机加载, 只要加载它们的类加载器不同, 那这两个类就必定不相等。
- 双亲委派机制保证java运行安全稳定。
参考:《深入理解Java虚拟机:JVM高级特性与最佳实践》
本文地址:https://blog.csdn.net/changlina_1989/article/details/112483881
推荐阅读