java八股文面试[JVM]——如何打破双亲委派模型

淡淡的烟草味﹌ 2024-03-26 23:10 172阅读 0赞
  1. 双亲委派模型的第一次“被破坏”是重写自定义加载器的loadClass(),jdk不推荐。一般都只是重写findClass(),这样可以保持双亲委派机制.而loadClass方法加载规则由自己定义,就可以随心所欲的加载类,典型的打破双亲委派模型的框架和中间件tomcatosgi
  2. 双亲委派模型的第二次“被破坏”是ServiceLoader和Thread.setContextClassLoader()。即线程上下文类加载器(contextClassLoader)。双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢?线程上下文类加载器就出现了。

    1. SPI。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。了有线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
    2. 线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。,所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题
  3. 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用
前言
  1. 比较两个类是否“相等”,前提是这两个类由同一个类加载器加载,
  2. 否则,即使这两个类来源于同一个Class 文件,被同一个虚拟机加载,
  3. 只要加载它们的类加载器不同,那么这两个类就必定不相等。
打破双亲委派

如下是一个自定义的类加载器TestClassLoader,并重写了findClass和loadClass:

  1. public class TestClassLoader extends ClassLoader {
  2. public TestClassLoader(ClassLoader parent) {
  3. super(parent);
  4. }
  5. @Override
  6. protected Class<?> findClass(String name) throws ClassNotFoundException {
  7. // 1、获取class文件二进制字节数组
  8. byte[] data = null;
  9. try {
  10. System.out.println(name);
  11. String namePath = name.replaceAll("\\.", "\\\\");
  12. String classFile = "C:\\study\\myStudy\\ZooKeeperLearning\\zkops\\target\\classes\\" + namePath + ".class";
  13. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  14. FileInputStream fis = new FileInputStream(new File(classFile));
  15. byte[] bytes = new byte[1024];
  16. int len = 0;
  17. while ((len = fis.read(bytes)) != -1) {
  18. baos.write(bytes, 0, len);
  19. }
  20. data = baos.toByteArray();
  21. } catch (FileNotFoundException e) {
  22. e.printStackTrace();
  23. } catch (IOException e) {
  24. e.printStackTrace();
  25. }
  26. // 2、字节码加载到 JVM 的方法区,
  27. // 并在 JVM 的堆区建立一个java.lang.Class对象的实例
  28. // 用来封装 Java 类相关的数据和方法
  29. return this.defineClass(name, data, 0, data.length);
  30. }
  31. @Override
  32. public Class<?> loadClass(String name) throws ClassNotFoundException{
  33. Class<?> clazz = null;
  34. // 直接自己加载
  35. clazz = this.findClass(name);
  36. if (clazz != null) {
  37. return clazz;
  38. }
  39. // 自己加载不了,再调用父类loadClass,保持双亲委托模式
  40. return super.loadClass(name);
  41. }
  42. }

测试:初始化自定义的类加载器,需要传入一个parent,指定其父类加载器,那就先指定为加载TestClassLoader的类加载器为TestClassLoader的父类加载器吧:

  1. public static void main(String[] args) throws Exception {
  2. // 初始化TestClassLoader,被将加载TestClassLoader类的类加载器设置为TestClassLoader的parent
  3. TestClassLoader testClassLoader = new TestClassLoader(TestClassLoader.class.getClassLoader());
  4. System.out.println("TestClassLoader的父类加载器:" + testClassLoader.getParent());
  5. // 加载 Demo
  6. Class clazz = testClassLoader.loadClass("study.stefan.classLoader.Demo");
  7. System.out.println("Demo的类加载器:" + clazz.getClassLoader());
  8. }

运行如下测试代码,发现报错了:
找不到java\lang\Object.class,我加载study.stefan.classLoader.Demo类和Object有什么关系呢?

转瞬想到java中所有的类都隐含继承了超类Object,加载study.stefan.classLoader.Demo,也会加载父类Object。Object和study.stefan.classLoader.Demo并不在同个目录,那就找到Object.class的目录(将jre/lib/rt.jar解压),修改TestClassLoader#findClass如下:
遇到前缀为java.的就去找官方的class文件。
078afca6f70b4931b39357ab16f2e7ce.png

运行测试代码:
还是报错了!!! 报错信息为:Prohibited package name: java.lang

f27d6e1c3e654721b8f1098f0b54ae11.png

看意思是java禁止用户用自定义的类加载器加载java.开头的官方类,也就是说只有启动类加载器BootstrapClassLoader才能加载java.开头的官方类。

得出结论,因为java中所有类都继承了Object,而加载自定义类study.stefan.classLoader.Demo,之后还会加载其父类,而最顶级的父类Object是java官方的类,只能由BootstrapClassLoader加载

跳过AppClassLoaderExtClassLoader
既然如此,先将study.stefan.classLoader.Demo交由BootstrapClassLoader加载即可
由于java中无法直接引用BootstrapClassLoader,所以在初始化TestClassLoader时,传入parent为null,也就是TestClassLoader的父类加载器设置为BootstrapClassLoader:

  1. package com.stefan.DailyTest.classLoader;
  2. public class Test {
  3. public static void main(String[] args) throws Exception {
  4. // 初始化TestClassLoader,并将加载TestClassLoader类的类加载器
  5. // 设置为TestClassLoader的parent
  6. TestClassLoader testClassLoader = new TestClassLoader(null);
  7. System.out.println("TestClassLoader的父类加载器:" + testClassLoader.getParent());
  8. // 加载 Demo
  9. Class clazz = testClassLoader.loadClass("com.stefan.DailyTest.classLoader.Demo");
  10. System.out.println("Demo的类加载器:" + clazz.getClassLoader());
  11. }
  12. }

双亲委派的逻辑在 loadClass,由于现在的类加载器的关系为TestClassLoader —>BootstrapClassLoader,所以TestClassLoader中无需重写loadClass。
运行测试代码:

b1092263cbfb401d9fb664919d2ec23b.png

成功了,Demo类由自定义的类加载器TestClassLoader加载的,双亲委派模型被破坏了。

如果不破坏双亲委派,那么Demo类处于classpath下,就应该是AppClassLoader加载的,所以真正破坏的是AppClassLoader这一层的双亲委派

一个比较完整的自定义类加载器

一般情况下,自定义类加载器都是继承URLClassLoader,具有如下类关系图:

ff51b37712454333a13146f22f749f78.png

tomcat是如何打破双亲委派的
#

Tomcat中可以部署多个web项目,为了保证每个web项目互相独立,所以不能都由AppClassLoader加载,所以自定义了类加载器WebappClassLoader,WebappClassLoader继承自URLClassLoader,重写了findClass和loadClass,并且WebappClassLoader的父类加载器设置为AppClassLoader。
WebappClassLoader.loadClass中会先在缓存中查看类是否加载过,没有加载,就交给ExtClassLoader,ExtClassLoader再交给BootstrapClassLoader加载;都加载不了,才自己加载;自己也加载不了,就遵循原始的双亲委派,交由AppClassLoader递归加载。

Web应用默认的类加载顺序是(打破了双亲委派规则):

先从JVM的BootStrapClassLoader中加载。
加载Web应用下/WEB-INF/classes中的类。
加载Web应用下/WEB-INF/lib/*.jap中的jar包中的类。
加载上面定义的System路径下面的类。
加载上面定义的Common路径下面的类。

如果在配置文件中配置了``,那么就是遵循双亲委派规则,加载顺序如下:

先从JVM的BootStrapClassLoader中加载。
加载上面定义的System路径下面的类。
加载上面定义的Common路径下面的类。
加载Web应用下/WEB-INF/classes中的类。
加载Web应用下/WEB-INF/lib/*.jap中的jar包中的类。

1 Tomcat对用户类库与类加载器的规划
在其目录结构下有三组目录(“/common/”、“/server/”、“/shared/”)可以存放Java类库,另外还可以加上Web应用程序本身的目录“/WEB-INF/”,一共4组,把Java类库放置在这些目录中的含义分别如下:

放置在/commom目录中:类库可被Tomcat和所有的Web应用程序共同使用
放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见
放置在/shared目录中:类库可被所有的Web应用程序所共同使用,但对Tomcat自己不可见
放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,所下图:

daea13a52b3d4677954e3a58977dd4fd.png

最上面的三个类加载器是JDK默认提供的类加载器,这三个加载器的的作用之前也说过,这里不再赘述了,而CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebAppClassLoader则是Tomcat自己定义的类加载器,他们分别加载/common/、/server/、/shared/和/WebApp/WEB-INF/中的Java类库。其中WebApp类加载器和jsp类加载器通常会存在多个实例每一个Web应用程序对应一个WebApp类加载器,每一个jsp文件对应一个Jsp类加载器

从上图的委派关系可以看出,CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的哪一个Class,它出现的目的就是为了被丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过在建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能

tomcat对于不同应用需要有不同的隔离环境
tomcat给每个应用都创建了一个WebApp ClassLoader类加载器
重写了load方法:不再向上查找,而是在本类查找不到后再向上。对于其他的需要共享的例如Redis,可以在上层Share ClassLoader中共享。

OSGI是如何打破双亲委派的
#

既然说到OSGI,就要来解释一下OSGi是什么,以及它的作用

OSGi(Open Service Gateway Initiative):是OSGi联盟指定的一个基于Java语言的动态模块化规范,这个规范最初是由Sun、IBM、爱立信等公司联合发起,目的是使服务提供商通过住宅网管为各种家用智能设备提供各种服务,后来这个规范在Java的其他技术领域也有不错的发展,现在已经成为Java世界中的“事实上”的模块化标准,并且已经有了Equinox、Felix等成熟的实现。OSGi在Java程序员中最著名的应用案例就是Eclipse IDE

OSGi中的每一个模块(称为Bundle)与普通的Java类库区别并不大,两者一般都以JAR格式进行封装,并且内部存储的都是Java Package和Class。但是一个Bundle可以声明它所依赖的Java Package(通过Import-Package描述),也可以声明他允许导出发布的Java Package(通过Export-Package描述)。在OSGi里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖(至少外观上如此),而且类库的可见性能得到精确的控制,一个模块里只有被Export过的Package才可能由外界访问,其他的Package和Class将会隐藏起来。除了更精确的模块划分和可见性控制外,引入OSGi的另外一个重要理由是,基于OSGi的程序很可能可以实现模块级的热插拔功能,当程序升级更新或调试除错时,可以只停用、重新安装然后启动程序的其中一部分,这对企业级程序开发来说是一个非常有诱惑性的特性

OSGi之所以能有上述“诱人”的特点,要归功于它灵活的类加载器架构。OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。例如,某个Bundle声明了一个它依赖的Package,如果有其他的Bundle声明发布了这个Package,那么所有对这个Package的类加载动作都会为派给发布他的Bundle类加载器去完成。不涉及某个具体的Package时,各个Bundle加载器是平级关系,只有具体使用某个Package和Class的时候,才会根据Package导入导出定义来构造Bundle间的委派和依赖

另外,一个Bundle类加载器为其他Bundle提供服务时,会根据Export-Package列表严格控制访问范围。如果一个类存在于Bundle的类库中但是没有被Export,那么这个Bundle的类加载器能找到这个类,但不会提供给其他Bundle使用,而且OSGi平台也不会把其他Bundle的类加载请求分配给这个Bundle来处理

一个例子:假设存在BundleA、BundleB、BundleC三个模块,并且这三个Bundle定义的依赖关系如下:

BundleA:声明发布了packageA,依赖了java.*的包
BundleB:声明依赖了packageA和packageC,同时也依赖了Java.*的包
BundleC:声明发布了packageC,依赖了packageA
那么,这三个Bundle之间的类加载器及父类加载器之间的关系如下图:
a4a4ac9fbf4641c18bf2ba6f0802b8ce.png

由于没有涉及到具体的OSGi实现,所以上图中的类加载器没有指明具体的加载器实现,只是一个体现了加载器之间关系的概念模型,并且只是体现了OSGi中最简单的加载器委派关系。一般来说,在OSGi中,加载一个类可能发生的查找行为委派关系会比上图中显示的复杂,类加载时的查找规则如下:

以java.*开头的类,委派给父类加载器加载
否则,委派列表名单内的类,委派给父类加载器加载
否则,Import列表中的类,委派给Export这个类的Bundle的类加载器加载
否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
否则,查找是否在自己的Fragment Bundle中,如果是,则委派给Fragment bundle的类加载器加载
否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载
否则,查找失败
从之前的图可以看出,在OSGi里面,加载器的关系不再是双亲委派模型的树形架构,而是已经进一步发展成了一种更复杂的、运行时才能确定的网状结构

相关面试题:一个类的静态块是否可能被执行两次

一个自于网易面试官的一个问题,一个类的静态块是否可能被执行两次。

答案:如果一个类,被两个 osgi的bundle加载, 然后又有实例被初始化,其静态块会被执行两次

什么是SPI 机制

Spi 机制加载第三方扩展的jar包类初始化。
mysql, dubbo rpc

SPi机制的原理:
java SPI全称Service Provider Interface 。是java 提供的一套用来被第三方实现的API,他可以用来启用框架扩展和替换组件。实际上是基于接口编程+策略模式+配置文件 组合实现的动态加载机制

JDBC

原本的JDBC: Class.forName(“DriverName”) 是通过调用Driver中静态代码块中的将Driver注册

  1. public class Driver extends NonRegisteringDriver implements java.sql.Driver {
  2. public Driver() throws SQLException {
  3. }
  4. static {
  5. try {
  6. DriverManager.registerDriver(new Driver());
  7. } catch (SQLException var1) {
  8. throw new RuntimeException("Can't register driver!");
  9. }
  10. }
  11. }

使用SPI的JDBC:
在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver,然后可以直接调用

Connection conn= DriverManager.getConnection(“jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK”, “root”, “”);

问题是 :一个类的加载器和调用他的加载器相同
这里调用的是 bootstrap类加载器,无法加载到子类厂商中的类

方法:使用线程上下文加载器

  1. public class DriverManager {
  2. static {
  3. loadInitialDrivers();
  4. println("JDBC DriverManager initialized");
  5. }
  6. private static void loadInitialDrivers() {
  7. //省略代码
  8. //这里就是查找各个sql厂商在自己的jar包中通过spi注册的驱动
  9. ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
  10. Iterator<Driver> driversIterator = loadedDrivers.iterator();
  11. try{
  12. while(driversIterator.hasNext()) {
  13. driversIterator.next();
  14. }
  15. } catch(Throwable t) {
  16. // Do nothing
  17. }
  18. //省略代码
  19. }
  20. }

使用Thread类的 getContextClassLoader

  1. public static <S> ServiceLoader<S> load(Class<S> service) {
  2. ClassLoader cl = Thread.currentThread().getContextClassLoader();
  3. return ServiceLoader.load(service, cl);
  4. }
  5. public static <S> ServiceLoader<S> load(Class<S> service,
  6. ClassLoader loader){
  7. return new ServiceLoader<>(service, loader);
  8. }

整个mysql的驱动加载过程:

第一,获取线程上下文类加载器,从而也就获得了应用程序类加载器(也可能是自定义的类加载器)
第二,从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
第三,通过线程上下文类加载器去加载这个Driver类,从而避开了双亲委派模型的弊端

SPI参考:39 如何破坏双亲委派机制原则 - 简书

知识来源:

JVM问题(一) — 如何打破双亲委派模型_如何打破双亲委派机制_leo_messi94的博客-CSDN博客

打破双亲委派的几种办法_破坏双亲委派_hhpub的博客-CSDN博客

发表评论

表情:
评论列表 (有 0 条评论,172人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JVM双亲委派模型

    什么是双亲委派模型? Java虚拟机对class文件采用的按需加载的方式,也就是说当需要使用该类的时候才会将它的class文件加载到内存当中,将加载此class文件时,它