类加载器的一、类加载器双亲委派模型
在 JDK 1.2 时期被引入,并被广泛应用于此后几乎所有的 Java 程序中,但它并不是一个具有强制性的约束力的模型,而是 Java 设计者们推荐给开发者的一种加载器实现的最佳实践。
Java 虚拟机设计团队有意把类加载阶段中的 “ 通过一个类的全限定名来获取描述该类的二进制字节流 ” 这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为 “ 类加载器 ”(Class Loader)。
判断两个类的是否 “ 相等 ”,是由类和加载类的类加载器共同决定的。二、双亲委派模型 2.1、双亲委派模型的建立 从虚拟机的角度,只有两种类加载器,一种启动类加载器(Bootstrap ClassLoader),使用 C++实现,是虚拟机的一部分,另一种是继承自抽象类
这里包括 Class 对象的 equal(),isAssignableFrom(),isInstance()和 instanceof 关键字的判断。
java.lang.ClassLoader
的启动器。但是在虚拟机实际中,提供了三种类加载器:
- 启动类加载器(Bootstrap Class Loader):主要负责加载存放在
目录下,或者被\lib -Xbootclasspath
参数指定的路径中存放的,Java虚拟机能识别的类名(如rt.jar、tools.jar
等),的类。(加载请求委派给引导类加载器时,使用null的。) - 扩展类加载器(Extension Class Loader):主要负责加载
目录下,或者被\lib\ext java.ext.dirs
系统变量所指定的路径中的所有类。sun.misc.Launcher.ExtClassLoader
- 应用程序类加载器(Application Class Loader):复杂加载用户类路径(Classpath)上所有的类库,开发者可以直接在代码中使用这个类加载器(
java.lang.ClassLoader#getSystemClassLoader
)。一般情况下系统默认的类加载器,也称 “ 系统类加载器 ”。sun.misc.Launcher.AppClassLoader
文章图片
而这种层级关系,是JDK 1.2以来,虚拟机设计者为开发者提供的类加载器最佳实践模型。这种类似父子关系的层级模型称为 “ 双亲委派模型 (Parents Delegation Model)”。这种父子关系并不是依赖于继承,而是使用的组合。
双亲委派模型的工作过程如下:
- 当前类加载器首先从自己已经加载的类(缓存)中,查询是否此类已经加载,如果已经加载,则直接返回原来已经加载的类。
- 如果在当前类加载器的缓存中,没有找到期待被加载的类时,则委托父类加载器去加载。父类加载器采用同样的策略,首先查看自己的缓存,(如果仍然没有)则继续委托其父类加载去加载,一直到 BootStrapClassLoader(启动类加载器)。
- 当所有的父类加载器都没有加载此类时,才由当前的类加载器加载,并将其放入自己的缓存中,以便下次有加载请求时直接返回。
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() {
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() {
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);
}
2.2、双亲委派模式优势 采用双亲委派模式的是好处是:
- Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以
避免类的重复加载
。 - 其次是考虑到安全因素,
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
2.3、自定义类加载器 通过继承
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){
thrownew 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世界里破坏双亲委派模型的三次情况:
3.1、JDK 1.2 之前
java.lang.ClassLoader
已经被引用,但是主要是通过重写 loadClass()
方法,这里还没有引入双亲委派模型。JDK1.2 以后 双亲委派模型 的逻辑写在了 loadClass()
方法内,在自定义加载类时,建议使用findClass()
方法。3.2、SPI(Service Provider Interface ) 为了能加载其他厂商实现并部署在应用程序的 ClassPath 下的 SPI。这些 SPI 是由启动类加载器加载,但是启动类加载器无法加载其他厂商代码,所以引入了线程上下文类加载器(Thread Context ClassLoader)。使用线程上下文加载器去加载 SPI 服务代码,是一种父类加载器去请求子类加载器完成类加载的行为,就违背了双亲委派模型的一般性原则,但是也无可奈何。例如:JDNI、JDBC、JCE、JAXB和JBI都是使用这种方式。
在 JDK 6时,JDK 提供了3.3、用户“动态性”概念 代码热替换(Hot Sweep)、模块热部署(Hot Deployment)等。java.util.ServiceLoader
类,以 META-INF/services 中的配置信息,辅以责任链模式,这才给 SPI 的加载提供了一种相对合理的解决方案。
OSGI
OSGI 实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块( OSGI 中称为 Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。
OSGI 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为复杂的网状结构,当收到类的加载请求时,OSGI 将按照下面的顺序经行类搜索:
- 将以
java.*
开头的类,委派给父类加载器加载。 - 否则,将委派列表单内的类,委派给父类加载器加载。
- 否则,将
Import
列表中的类,委派给Export
这个类的Bundle
的类加载器加载。 - 否则,查找当前
Bundle
的ClassPath
,使用自己的类加载器加载。 - 否则,查找类是否在自己的
Fragment Bundle
中,如果在,则委派给Fragment Bundle
的类加载器加载。 - 否则,查找
Dynamic Import
列表的Bundle
,委派给对应的Bundle
的类加载器加载。 - 否则,类查找失败。
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
- 【JVM|双亲委派模型(Parents Delegation Model)(JDK 8)】真正理解线程上下文类加载器(多案例分析)
推荐阅读
- jvm|【JVM】JVM08(java内存模型解析[JMM])
- jvm|JVM调优(线上 JVM GC 频繁耗时长,出现 LongGC 告警,这次排查后想说:还有谁(...))
- java内存区域与内存溢出异常
- JVM|JVM优化(一)
- 自动内存管理机制
- JVM: 使用 jstack 命令找出 cpu 飙高的原因
- java|JVM之字节码如何在jvm流转
- jvm|从栈帧看字节码是如何在 JVM 中进行流转的
- java|[NIO和Netty] NIO和Netty系列(二): Java Reference详解
- 生活|jrebeleclipse/tomcat 使用方法