java之双亲委派机制
什么是双亲委派机制
jvm对class文件采用的是按需加载的方式,当需要该类时才会将它的class文件加载到内存中。
加载时采用的是双亲委派机制
,即将请求交给父类处理的任务委派模式。
双亲委派机制的原理图如下:
双亲委派机制的原理
- 类加载器收到类加载请求,先检查是否有父类加载器,若有,则将请求委托给父类加载器加载。
- 如果父类还有父类,则继续向上委托,直至
引导/启动类加载器 Bootstrap ClassLoader
。 - 如果父加载器可以完成加载,则返回成功结果。否则,由子类自行加载。如果子类加载失败则会抛出
ClassNotFoundException
。
加过过程图解如下:
对应源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42public abstract class 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 {
//最顶级父类bootstrapClassLoader加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//父类中既没有缓存,也无法加载,则当前classloader尝试加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//自定义classLoader只能重写findClass方法
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、findClass、defineClass方法的区别
- loadClass: 内置双亲委派的实现机制
- findClass:根据名称或者路径加载
.class
文件 - defineClass:把
.class
字节码转化为Class
对象
双亲委派机制的优势?
- 避免类的重复加载:通过双亲委派机制可以避免类的重复加载,当父类已经加载过某一个类时,子类加载器就不会再次加载该类。
- 保护程序安全,防止核心API被随意篡改: 引导类加载器加载时,只加载
JAVA_HOME
中的jar包,例如java.lang.String
。
双亲委派机制的缺点?
子类可以使用父类加载的类,但是父类无法使用自类加载器加载过的类。
JAVA中有许多SPI接口,允许第三方提供实现。比如数据库实现JDBC
。这些接口由java核心类库提供,交由第三方实现。如果沿用双亲委派机制,就会存在提供者由bootstrap ClassLoader
加载,而实现由第三方自定义类加载器加载,此时,顶层类加载器就无法使用子类加载器加载的类。 此时就需要打破双亲委派机制。
什么是反向委派机制
java中存在很多Service Provider Interface,SPI
,这些借口允许第三方提供实现。比如jdbc、jndi
等这些SPI
属于java核心接口,一般存在于rt.jar
中,由BootStrapClassloader
加载。而BootStrapClassloader
无法直接加载SPI
的实现类。同时由于双亲委派机制的存在,BootStrapClassloader
也无法反向委托应用类加载器加载SPI
的实现类。
那该如何加载SPI
的实现类呢?
通过委派线程上下文类加载器
把jdbc.jar
中的实现类加载到内存中以供使用。这个过程中摒弃了原双亲委派机制,程序可以逆向的加载类,故而称之为反向委派
。
打破双亲委派机制
打破双亲委派机制有如下两种方式:
- 重写
loadClass
方法 - 使用
线程上下文类加载器
重写loadClass方法
双亲委派的实现在loadClass
中,自定义类加载器,重写加载逻辑避免继续执行原生类加载过程即可。
使用 线程上下文类加载器
利用线程上下文加载器(Thread Context ClassLoader)
也可以打破双亲委派。
Java 应用上下文加载器默认是使用 AppClassLoader
。若想要在父类加载器使用到子类加载器加载的类,可以使用 Thread.currentThread().getContextClassLoader()
线程上下文类加载器
反向双亲委派机制的使用场景
1. 解决依赖冲突
详见 https://juejin.cn/post/6931972267609948167
2. tomcat打破双亲委派机制
打破双亲委派机制的场景很多: JDBC、JNDI、Tomcat等,我们以Tomcat为例来说明
tomact是一个web容器,主要解决以下问题:
- 一个web容器可能要部署两个或多个应用程序,不同的应用程序之间可能会依赖同一个第三方类库的不同版本,因此要保障每个应用程序的类库是独立的、相互隔离的。
- 不输在同一个web容器的相同的类库的相同版本可以共享,否则会有重复的类库被加载进jvm中。
- web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔离。
- web容器支持jssp文件修改后不用重启,jsp文件也要编译为.class文件,支持hotswap功能。
而默认的类加载器无法加载两个相同类库的不同版本,因为加载时是以类的全限定名来确定的,无法处理上述的隔离问题。
同时在修改jsp文件后,因为类名一致,默认的类加载器不会重新加载,而是使用方法区中已经存在的类,所以每一个jsp都需要对应一个唯一的类加载器,当修改jsp文件时,需要卸载此唯一的类加载器,然后重新创建类加载器,并加载修改后的jsp文件。
tomcat的类加载器结构
上图中自上而下有五个自定义类加载器:
- CommonClassLoader: tomcat中最基本的类加载器,加载路径中 class可以被tomcat和各个webapp访问
- CatalinaClassLoader: tomcat私有类加载器,对webapp不可见。webapp不能访问其加载路径下的class
- SharedClassLoader: 各个webapp共享的类加载器,但tomcat不可见
- webAppClassLoader: webapp私有的类加载器,仅对当前webapp可见
- JasperClassLoader:JSP的类加载器
其中,每个web程序都有一个webappClassLoader,每个jsp文件都有一个JasperClassLoader,这两个类加载器有多个实例。
tomcat打破双亲委派机制的工作原理
- CommonClassLoader能加载的类都可以被CatalinaClassLoader使用,从而实现了公有类库的共用。
- CatalinaClassLoader 和 SharedClassLoader 加载的类相互隔离,以此保障了tomcat和web容器的隔离性。
- webAppClassLoader可以使用 SharedClassLoader 加载的类,但是多个 webAppClassLoader 加载的类 相互隔离。
- JasperClassLoader的加载范围仅限于这个JSP文件所编译出的.class文件。当web容器检测到JSP文件被修改时,会替换掉之前的JasperClassLoader实例,再创建一个Jsp类加载器来实现JSP文件的HotSwap功能
tomcat的目录结构:
- /common/*
- /server/*
- /shared/*
- /WEB-INF/*
默认情况下,conf目录下的catalina.properties文件,没有指定server.loader以及shared.loader,所以tomcat没有建立CatalinaClassLoader和SharedClassLoader实例,这两个都会使用CommonClassLoader来代替。Tomcat6之后,把common、shared、server目录合成一个lib目录,所以我们服务器里看不到common、shared、server目录。
tomcat应用的默认加载顺序
- 从JVM的 BootStrapClassLoader 中加载
- 加载 web应用目录下 /WEB-INF/classes 中的类
- 加载 web目录下 /WEB-INF/lib/*.jar 中的 jar包中的类
- 委托给 system、common类加载器去加载
tomcat 类加载过程
- 在本地缓存中查找是否已经加载过该类(加载过会被缓存在resourceEntries中)
- 如果没有,尝试使用 引导类加载器 尝试加载该类
- 如果没有,使用当前类加载器加载 webAppClassLoader 加载
- 如果未加载,则遵循双亲委派机制 委派给 AppClassLoader此时加载顺序是 AppClassLoader、commonClassLoader、SharedClassLoader 进行加载
3. 热加载
4. 热部署
5. 加密保护
SPI
什么是SPI
服务提供接口(SPI,Service Provider Interface
) 是 JDK 内置的一种「服务提供发现机制」,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件(可通过 SPI 机制实现模块化)。SPI 的整体机制图如下: