类加载器
加载过程
-
检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
-
如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
-
请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
-
请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
-
当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
-
从文件中载入Class,成功后跳至第8步。
-
抛出ClassNotFountException异常。
-
返回对应的java.lang.Class对象。
在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
分类
C++编写
启动类加载器-Bootstrap ClassLoader:
- 没有实体,JVM将C++处理类加载的一套逻辑定义为启动类加载器;由C++编写的,通过Java程序去查看显示的是null
- 通过启动类加载器加载类sun.launcher.LauncherHelper,执行该类的方法checkAndLoadMain,加载main函数所在的类,启动扩展类加载器、应用类加载器也是在这个时候完成的
- 可以通过-Xbootclasspath指定
- 用来加载 Java 的核心库
- 负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class
java编写,继承自java.lang.ClassLoader
扩展类加载器-Extension ClassLoader:
-
可以通过java.ext.dirs指定
-
用来加载 Java 的扩展库
-
负责加载JRE的扩展目录,lib/ext
应用类加载器-Application ClassLoader: -
可以通过java.class.path指定
-
根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。
自定义类加载器-User ClassLoader
双亲委派
双亲委派机制
原理:
双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
try {
if (this.parent != null) {
c = this.parent.loadClass(name, false);
} else {
c = this.findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException var10) {
}
优势
- 避免重复加载
Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。 - 避免核心类篡改
假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
打破双亲委派
1. 自定义类加载器,重写loadClass方法
- loadClass:双亲委派机制就是通过该方法实现的。默认过程-先判断该类是否被当前层的类加载过,如果没有就将该类委托给父类加载器。如果最顶端父类无法加载则向下传递。
重写方法后可以自己定义是用什么加载器,也可以自定义委派机制。 - findclass
这只是一个空方法,返回内容为class,方法其中没有任何内容,只抛出了个异常,说明这个方法需要开发者自己去实现。
如果自定义的方法不想违背双亲委派模型,则只需要重写findclass方法即可,如果想违背双亲委派模型,则还需要重写loadclass方法。
if (name.startsWith("com.luban")) {
// com.luban包下的所有类不委派
c = findClass(name);
} else {
c = this.getParent().loadClass(name);
}
线程上下文类加载器破坏双亲委派机制
SPI
- 概念
约定在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名。这样当我们引用了某个 jar 包的时候就可以去找这个 jar 包的 META-INF/services/ 目录,再根据接口名找到文件,然后读取文件里面的内容去进行实现类的加载与实例化。 - 缺点
Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。 - 实现
- 当服务提供者提供了接口的一种具体实现后,在 jar 包的 META-INF/services 目录下创建一个以 “接口全限定名” 命名的文件,内容为实现类的全限定名;
- 接口实现类所在的 jar 包放在主程序的 classpath 中;
- 主程序通过 java.util.ServiceLoder 动态装载实现模块,它通过扫描 META-INF/services 目录下的配置文件找到实现类的全限定名,把类加载到 JVM ;
- SPI 的实现类必须携带一个不带参数的构造方法;
- 线程上下文类加载器引入原因
Java 提供了很多服务提供者接口(SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,
SPI 实现的 Java 类一般是由系统类加载器来加载的。启动类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
优点
使用线程上下文类加载器,可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。典型的例子有:通过线程上下文来加载第三方库jndi实现,而不依赖于双亲委派。
操作方法
ServideLoader—
获取
Thread.currentThread().getContextClassLoader()
设置
Thread.currentThread().setContextClassLoader(new Classloader_4());