- 参考:
- 深入理解 Tomcat类加载器为何违背双亲委派模型 - https://baijiahao.baidu.com/s?id=1709297949558383890
文章目录
JVM中类的加载
JVM(Java Virtual Machine,Java 虚拟机)把描述类的数据从.class文件
加载进内存,并对数据进行校验,转换解析和初始化,最终形成可以被JVM直接使用的Java类对象
,这就是虚拟机的类加载机制。
💡 JVM设计团队把类加载阶段中的 “通过一个类的全限定名来获取描述此类的二进制字节流” 这个动作放到JVM外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这动作的代码模块成为“类加载器(ClassLoader)”。
JVM中类的唯一性
在JVM中,每一个类加载器,都拥有一个独立的类命名空间。因此,对于任意一个类,都需要由加载他的类加载器(ClassLoader)和这个类(.class)本身一同确立其在Java虚拟机中的唯一性。
换句话说,比较两个类是否“相等”:
- 判断它们是否来自同一个类加载器
- 判断它们是否来自同一个Class文件
只有这两点都相同,JVM才会认为它们是同一个类
JVM中类的类加载器
从JVM的角度来说,只存在两种不同类加载器:
- 一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(只限HotSpot),是虚拟机自身的一部分;
- 另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类
java.lang.ClassLoader
.
从Java开发人员的角度来看,类加载还可以划分成以下3种系统提供的类加载器:
-
启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在
JAVA_HOME/lib
目录中的类(.class
)/类库(.jar
),或者被-Xbootclasspath
参数所指定的路径中的类/类库💡 提示
这些路径上的类还需要是虚拟机能识别的。如rt.jar,名字不符合的类库即使放在lib目录下也不会重载
-
扩展类加载器(Extension ClassLoader):这个类加载器由
sun.misc.Launcher$ExtClassLoader
实现,它负责加载JAVA_HOME/lib/ext
目录下的,或者被java.ext.dirs
系统变量所指定的路径种的所有类库。开发者可以直接使用扩展类加载器。 -
应用程序类加载器(Application ClassLoader):这个类加载器由
sun.misc.Launcher$AppClassLoader
实现。由于这个类加载器是 ClassLoader 中的getSystemClassLoader方法
的返回值,所以也称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用中没有定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
图中各个类加载器之间的关系称为类加载器的 双亲委派模型(Parents Dlegation Mode)
JVM中类的类加载机制 - 双亲委任模型
在Java2之后的版本中,类的加载采用的是一种称为双亲委派的代理模型:
- 除了顶层的启动类加载器(BootStrap)之外,其他ClassLoader在加载类前,先委派给双亲去加载类;
- 只有当父加载器反馈自己无法完成这个请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
💡 提示
这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。
# 目的: 保证类的唯一性
如果没有使用双亲委派模型,由各个类加载器自行加载的话,不同类加载器可能加载相同类标识(package+classname)的类,(由于类标识相同)这些类可能会被其他依赖它的类误用,这会导致一系列无法预计的混乱。
💡 提示
如果类能到处被调用,甚至存在安全问题
因此,要通过双亲委派机制保证类的唯一性。
# 实现双亲委任模型
所有的代码都在 java.lang.ClassLoader
的 loadClass方法之中
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 💡 获取锁。理想情况:相同的name相同的锁,不同的name不同的锁
synchronized (getClassLoadingLock(name)) {
// 💡 查看这个类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
// 💡 如果没有加载,执行加载逻辑
long t0 = System.nanoTime();
try {
// ⭐️ (双亲委派) ⭐️
// 💡 如果有父类ClassLoader,优先给父类进行类加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 💡 如果没有父类ClassLoader,就交给Bootstrap进行类加载
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();
// ⚠️注意: 这个方法是会抛出ClassNotFoundException异常的
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;
}
}
# 打破双亲委任模型的情况
JNDI
没看懂:
是这个模型自身的缺陷导致的。我们说,双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API, 但没有绝对,如果基础类调用会用户的代码怎么办呢?
这不是没有可能的。一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时就放进去的rt.jar),但它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识“这些代码啊。因为这些类不在rt.jar中,但是启动类加载器又需要加载。怎么办呢?
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader方法进行设置。如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过多的话,那这个类加载器默认即使应用程序类加载器。
嘿嘿,有了线程上下文加载器,JNDI服务使用这个线程上下文加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。但这无可奈何,Java中所有涉及SPI的加载动作基本胜都采用这种方式。例如JNDI,JDBC,JCE,JAXB,JBI等。
热部署
为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。
OSGI
todo https://blog.csdn.net/woniu211111/article/details/118036346
# 打破双亲委任模型的方法
Tomcat实现
tomcat 为了实现加载类的隔离性,没有遵守双亲委派模型约定。而是要求每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器
在 AppClassLoader 后面添加了一系列tomcat的类加载器:
⚠️ 注意
- CommonClassLoader、CatalinaClassLoader、SharedClassLoader分别加载/common/*、/server/*、/shared/*。但是,在tomcat 6以及之后的版本中,已经合并到根目录下的lib目录下
- WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
- JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
类加载器的构建过程可以在源码中看到:
org.apache.catalina.startup.Bootstrap.initClassLoaders
private void initClassLoaders() {
try {
逻辑父类为null
↓
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader = this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
扩展问题: 如果tomcat 的 Common ClassLoader 想加载 WebApp ClassLoader 中的类,该怎么办?
看了前面的关于破坏双亲委派模型的内容,我们心里有数了,我们可以使用线程上下文类加载器实现,使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。
todo JNDI原理
在有双亲委派模式的情况下,启动类装载器(ClassLoader)可以抢在标准扩展类装载器之前去装载类,而标准扩展类装载器可以抢在系统类装载器之前去装载那个类,类路径类装载器又可以抢在用户自定义类装载器之前去装载它,用这种方法,类装载器的体系结构就可以防止不可靠的代码用它们自己的版本来替代可信任的类。
众所周知,启动tomcat方法是运行bootstrap的main方法
$ javaw \
-Dcatalina.home=E:\temp1\apache-tomcat-8.5.84-src \
-Dcatalina.base=E:\temp1\apache-tomcat-8.5.84-src \
org.apache.catalina.startup.Bootstrap start \
其中,如果如果 ${catalina.base}/xxxxwebapp/WEB-INF/lib/*.jar 中有jar包,会被加载到运行环境。
- 如何加载这些jar包?
- 如果有多个webapp,它们依赖的jar包不一样,如何避免运行环境中jar包的冲突?
- 上述问题的解决方法: 打破双亲委派机制