「JVM 类加载子系统」系列文章
通过分析源码可以知道双亲委派模型就是在loadClass
中实现的,如果想打破,直接重写自己的loadClass
逻辑即可
在前文 自定义类加载器 的基础上,重写loadClass
方法,其他不变:
xxxxxxxxxx
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) {
// 如果是 jvm 自带的类还是需要通过双亲委派模型加载
// 自己写的类可以打破该模型
if (!name.startsWith("com.lfool.myself")) {
return this.getParent().loadClass(name);
} else {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
此时,就算不把类路径下对应的com.lfool.myself.Test03
删掉,也会由自定义类加载器加载,而不会由应用程序类加载器加载
通过前文 双亲委派模型 介绍可以知道,JVM 使用「全盘委托机制」,但现在存在一个问题,如果有基础类需要回调用户的代码,怎么办???
假设基础类的类加载器是启动类加载器,根据「全盘委托机制」,该类所依赖及引用的类也由这个 CladdLoader 加载,但该类所引用的是用户代码,启动类加载器根本加载不了
所以这就涉及到父类加载器去请求子类加载器完成类加载行为,打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则
热部署是指在项目不重启的前提下,更改部分内容,还能使项目顺利运行
OSGi 实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块 (OSGi 中称为 Bundle) 都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换
在 OSGi 环境下,类加载器不再使用双亲委派模型推荐的树状结构,而是进一步发展为更为复杂的网状结构
Tomcat 也是一个 Java 程序,启动一个 Tomcat 服务,首先创建一个 JVM 进程,然后 Tomcat 服务运行在 JVM 中
部署在 Tomcat 中的多个应用以线程的形式存在,每个应用都有自己单独的来加载器,所以应用与应用之间无法相互调用,实现了类的隔离
通常,我们会在一个tomcat下部署多个应用,而这多个应用可能使用的类库的版本是不同的
比如:项目 A 使用的是 Spring4,项目 B 使用的是 Spring5。Spring4 和 Spring5 多数类都是一样的,但是有个别类有所不同,这些不同是类的内容不同,而类名,包名都是一样的
假如,我们采用 JDK 向上委托的方式,项目 A 在部署的时候,应用类加载器加载了它的类;在部署项目 B 的时候,由于类名相同,这时应用服务器就不会再次加载同包同名的类,这样就会有问题
所以,Tomcat 需要打破双亲委派机制,不同的 war 包下的类自己加载,而不向上委托,但是基础类依然向上委托
上图中,蓝色部分依旧和原来一样,使用双亲委派模型;绿色部分是 Tomcat 第一部分自定义的类加载器,这一部分加载 Tomcat 包中的类,依旧采用双亲委派模型;紫色部分是 Tomcat 第二部分自定义的类加载器,正是这一部分,打破了双亲委派模型
Tomcat 第一部分自定义类加载器 (绿色部分)
在 Tomcat6 之前,这三个类加载器在三个不同的文件夹下;而 Tomcat6 及以后这三部分合并到了一个文件夹下
下面介绍这三部分的作用:
CommonClassLoader:Tomcat 最基本的类加载器,加载路径中的 class 可以被 Tomcat 容器本身和各个 webapp 访问
CatalinaClassLoader:Tomcat 容器中私有的类加载器,加载路径中的 class 对于 webapp 不可见的部分
SharedClassLoader:各个 webapps 共享的类加载器,加载路径中的 class 对于所有的 webapp 都可见,但是对于 Tomcat 容器不可见
这一部分类加载器,依然采用的是双亲委派机制。原因:它只有一份,如果有重复,那么也是以这一份为准,这部分主要加载的是 Tomcat 自带的类
Tomcat 第二部分自定义类加载器 (紫色部分)
紫色部分是项目在打成 war 包时,Tomcat 自动生成的类加载器,专门来加载这个 war 包,而这个类加载器打破了双亲委派模型
假设如果没有打破,一旦两个项目中有两个相通类名,但内容不同的类时,只会加载其中一个
前文 自定义类加载器 中介绍过如何自定义类加载器,该部分在前文的基础上,修改一丢丢即可
在两个不同的包中,有两个相同的类,关系如下:
xxxxxxxxxx
➜ myself pwd
/Users/lfool/myself/temp/Java/test01/com/lfool/myself
➜ myself ls
Test.java
# ------
➜ myself pwd
/Users/lfool/myself/temp/Java/test02/com/lfool/myself
➜ myself ls
Test.java
两个类的内容如下:
xxxxxxxxxx
package com.lfool.myself;
public class Test {
public void f() {
System.out.println("Test01 中的 Test");
}
}
// ------
package com.lfool.myself;
public class Test {
public void f() {
System.out.println("Test02 中的 Test");
}
}
对应两个不同的类加载器:
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
MyClassLoader myClassLoader1 = new MyClassLoader("/Users/lfool/myself/temp/Java/test01");
Class<?> c1 = myClassLoader1.loadClass("com.lfool.myself.Test");
Object obj1 = c1.newInstance();
Method f1 = c1.getDeclaredMethod("f", null);
f1.invoke(obj1, null);
System.out.println(c1.getClassLoader().getClass().getName());
MyClassLoader myClassLoader2 = new MyClassLoader("/Users/lfool/myself/temp/Java/test02");
Class<?> c2 = myClassLoader2.loadClass("com.lfool.myself.Test");
Object obj2 = c2.newInstance();
Method f2 = c2.getDeclaredMethod("f", null);
f2.invoke(obj2, null);
System.out.println(c2.getClassLoader().getClass().getName());
}
// result
Test01 中的 Test
com.lfool.myself.MyClassLoader
Test02 中的 Test
com.lfool.myself.MyClassLoader