类加载器的
双亲委派模型
在 JDK 1.2 时期被引入,并被广泛应用于此后几乎所有的 Java 程序中,但它并不是一个具有强制性的约束力的模型,而是 Java 设计者们推荐给开发者的一种加载器实现的最佳实践。
Java 虚拟机设计团队有意把类加载阶段中的 “ 通过一个类的全限定名来获取描述该类的二进制字节流 ” 这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为 “ 类加载器 ”(Class Loader)。
判断两个类的是否 “ 相等 ”,是由类和加载类的类加载器共同决定的。
这里包括 Class 对象的 equal(),isAssignableFrom(),isInstance()和 instanceof 关键字的判断。
从虚拟机的角度,只有两种类加载器,一种启动类加载器(Bootstrap ClassLoader),使用 C++实现,是虚拟机的一部分,另一种是继承自抽象类 java.lang.ClassLoader
的启动器。
但是在虚拟机实际中,提供了三种类加载器:
\lib
目录下,或者被-Xbootclasspath
参数指定的路径中存放的,Java虚拟机能识别的类名(如rt.jar、tools.jar
等),的类。(加载请求委派给引导类加载器时,使用null的。)\lib\ext
目录下,或者被java.ext.dirs
系统变量所指定的路径中的所有类。sun.misc.Launcher.ExtClassLoader
java.lang.ClassLoader#getSystemClassLoader
)。一般情况下系统默认的类加载器,也称 “ 系统类加载器 ”。sun.misc.Launcher.AppClassLoader
在虚拟机内部,加载器之间存在一定的关系如下:
而这种层级关系,是JDK 1.2以来,虚拟机设计者为开发者提供的类加载器最佳实践模型。这种类似父子关系的层级模型称为 “ 双亲委派模型 (Parents Delegation Model)”。这种父子关系并不是依赖于继承,而是使用的组合。
双亲委派模型的工作过程如下:
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;
}
}
虚拟机中三大类加载器建立双亲委派模型的过程源码:
sun.misc.Launcher
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//初始化【扩展类加载器】,没有入参,使用的是【启动类加载器】
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 使用【扩展类加载器】初始化【应用程序类加载器】
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 使用【应用程序类加载器】初始化【线程上下文类加载器】
Thread.currentThread().setContextClassLoader(this.loader);
//这是部分代码
}
扩展类加载器创建过程:
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
final File[] var0 = getExtDirs();
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
int var1 = var0.length;
for(int var2 = 0; var2 < var1; ++var2) {
MetaIndex.registerDirectory(var0[var2]);
}
return new Launcher.ExtClassLoader(var0);
}
});
} catch (PrivilegedActionException var2) {
throw (IOException)var2.getException();
}
}
public ExtClassLoader(File[] var1) throws IOException {
// classLoader 为null 就是默认使用启动类加载器
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
应用程序类加载:
// 这里创建时,传入的是扩展类加载器
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
AppClassLoader(URL[] var1, ClassLoader var2) {
//使用扩展类加载器作为父加载器创建
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
采用双亲委派模式的是好处是:
避免类的重复加载
。java核心api中定义类型不会被随意替换
。假设通过网络传递一个名为java.lang.Integer
的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer
,而直接返回已加载过的 Integer.class
,这样便可以防止核心 API 库被随意篡改。可能你会想,如果我们在 classpath 路径下自定义一个名为java.lang.SingleInterge
类(该类是胡编的)呢?该类并不存在java.lang
中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang
是核心 API 包,需要访问权限,强制加载将会报出如下异常
java.lang.SecurityException: Prohibited package name: java.lang
通过继承java.lang.ClassLoader
抽象类来实现。主要介绍一下核心方法
loadClass(java.lang.String)
该方法加载指定名称(包括包名)的二进制类型。JDK1.2 以后不要建议重写。负责构建双亲委派模型。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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;
}
}
protected Class> findClass(String name)
但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected final Class> defineClass(String name, byte[] b, int off, int len)
defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。
举个例子
package com.hyl.learnerJVM.load;
import java.io.IOException;
import java.io.InputStream;
/**
* 类加载器与 instanceof 关键词演示
*
* @author hyl
* @version v1.0: ClassLoaderTest.java, v 0.1 2020/8/13 13:30 $
*/
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("com.hyl.learnerJVM.load.ClassLoaderTest").newInstance();
System.out.println( obj.getClass());
System.out.println( obj instanceof com.hyl.learnerJVM.load.ClassLoaderTest);
ClassLoader myLoader2 = ClassLoader.getSystemClassLoader();
Object obj2 = myLoader2.loadClass("com.hyl.learnerJVM.load.ClassLoaderTest").newInstance();
System.out.println( obj2.getClass());
System.out.println( obj2 instanceof com.hyl.learnerJVM.load.ClassLoaderTest);
Object obj3 =ClassLoaderTest.class.getClassLoader().loadClass("com.hyl.learnerJVM.load.ClassLoaderTest").newInstance();
System.out.println( obj3.getClass());
System.out.println( obj3 instanceof com.hyl.learnerJVM.load.ClassLoaderTest);
}
}
Java世界里破坏双亲委派模型的三次情况:
java.lang.ClassLoader
已经被引用,但是主要是通过重写 loadClass()
方法,这里还没有引入双亲委派模型。JDK1.2 以后 双亲委派模型 的逻辑写在了 loadClass()
方法内,在自定义加载类时,建议使用findClass()
方法。
为了能加载其他厂商实现并部署在应用程序的 ClassPath 下的 SPI。这些 SPI 是由启动类加载器加载,但是启动类加载器无法加载其他厂商代码,所以引入了线程上下文类加载器(Thread Context ClassLoader)。使用线程上下文加载器去加载 SPI 服务代码,是一种父类加载器去请求子类加载器完成类加载的行为,就违背了双亲委派模型的一般性原则,但是也无可奈何。例如:JDNI、JDBC、JCE、JAXB和JBI都是使用这种方式。
在 JDK 6时,JDK 提供了
java.util.ServiceLoader
类,以 META-INF/services 中的配置信息,辅以责任链模式,这才给 SPI 的加载提供了一种相对合理的解决方案。
代码热替换(Hot Sweep)、模块热部署(Hot Deployment)等。
OSGI
OSGI 实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块( OSGI 中称为 Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。
OSGI 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为复杂的网状结构,当收到类的加载请求时,OSGI 将按照下面的顺序经行类搜索:
java.*
开头的类,委派给父类加载器加载。Import
列表中的类,委派给 Export
这个类的 Bundle
的类加载器加载。Bundle
的 ClassPath
,使用自己的类加载器加载。Fragment Bundle
中,如果在,则委派给 Fragment Bundle
的类加载器加载。Dynamic Import
列表的 Bundle
,委派给对应的 Bundle
的类加载器加载。1 和 2 还是遵从双亲委派模型,后面就没有了。
JDK9 ,开始了Java模块化系统(Java Platform Module System,JPMS)算是
第四次破坏双亲委派模型
,在加载时要先判断能够归属到某一个系统模块中,如果存在这种归属关系,就优先委派给负责那个模块的加载器完成加载。
破坏双亲委派模型不一定是贬义词,只要有明确的目的和充分的理由,突破旧有原则无疑也是一种创新。
《深入理解Java虚拟机》第三版,周志明著。
https://www.dazhuanlan.com/2019/11/17/5dd0248ad6d20/
https://zhuanlan.zhihu.com/p/73359363
https://docs.oracle.com/javase/tutorial/ext/basics/load.html
https://blog.csdn.net/javazejian/article/details/73413292
真正理解线程上下文类加载器(多案例分析)