面试官:如何打破双亲委派机制?

· 浏览次数 : 50

小编点评

面试中可能会问到有关类加载机制的问题,包括双亲委派机制的含义、如何打破双亲委派机制、JVM中的类加载器以及自定义类加载器的构造等。下面是对这些问题的详细解答: ### 双亲委派机制 双亲委派机制是Java类加载器中的一个重要概念。它的主要作用是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终会传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子类加载器才会尝试自己去加载该类。 ### 如何打破双亲委派机制 打破双亲委派机制通常意味着自定义类加载器需要重写`loadClass`方法,而不是`findClass`方法。这是因为`loadClass`方法提供了更多的灵活性,允许加载类时进行一些额外的处理,比如替换掉标准库中的类,或者根据需要自定义类的加载逻辑。 ### JVM中的类加载器 JVM中有三种主要的类加载器: 1. **启动类加载器(Bootstrap ClassLoader)**:它负责加载JDK安装目录下的核心类库(如java.lang.*等)。 2. **扩展类加载器(Extension ClassLoader)**:它负责加载JDK安装目录外的扩展类库(如javax.*等),这些类库通常由Java的扩展目录提供。 3. **应用程序类加载器(Application ClassLoader)**:它负责加载用户类路径(ClassPath)上的类。对于没有自定义类加载器的应用程序,这是默认的类加载器。 ### 自定义类加载器 自定义类加载器需要继承`ClassLoader`类,并重写`findClass`方法来定义自己的加载逻辑。如果希望打破双亲委派机制,还需要重写`loadClass`方法。 ### Tomcat类加载机制 Tomcat的类加载机制涉及到多个类加载器,包括Common类加载器、Catalina类加载器和Shared类加载器。Common类加载器用于加载公共类库,Catalina类加载器用于加载Tomcat自身的类,而Shared类加载器用于加载Web应用之间共享的类。这种设计有助于实现类库的共享和隔离,同时也保证了安全性和性能。 ### Class.forName()和ClassLoader.loadClass()区别 - `Class.forName()`会将类的.class文件加载到JVM中,并进行解释和执行其中的静态代码块。 - `ClassLoader.loadClass()`只会将.class文件加载到JVM中,而不会执行静态代码块。只有在实例化类时,才会执行其中的静态代码块。 ### 总结 类加载机制是Java运行时环境的重要组成部分,它负责将字节码文件转换为Java虚拟机可以执行的类。双亲委派机制是Java类加载的一个经典设计,它保证了类加载的一致性和安全性。然而,在某些情况下,可能需要打破双亲委派机制,这时可以通过重写类加载器的`loadClass`方法来实现。

正文

面试连环call

  1. 双亲委派机制是什么?如何打破双亲委派机制?
  2. JVM都有哪些类加载器
  3. 如何构造一个自定义类加载器
  4. Tomcat的类加载机制?Spring的类加载机制
  5. Class.forName()和ClassLoader.loadClass()区别?

在开始讲述之前简单回顾一下之前的类加载过程

  • 类加载过程:加载->连接->初始化
  • 其中连接过程又分为:验证->准备->解析

具体内容大家可以看我上一篇关于类加载过程的详细介绍 Java类是如何被加载到内存中的?

类加载器作用

类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。

字节码可以是 Java 源程序(.java文件)经过 javac 编译得来,也可以是通过工具动态生成或者通过网络下载得来。

image-20211030235925336

需要注意的是

  • 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。

  • 每个 Java 类都有一个引用指向加载它的 ClassLoader

  • 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。

Class.forName()和ClassLoader.loadClass()区别?

  • Class.forName(): 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块
  • ClassLoader.loadClass(): 只是将.class文件加载到jvm中,不会执行static中的内容, 只有在newInstance才会去执行static中内容

类加载器分类

  1. 启动类加载器: Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

  2. 扩展类加载器: Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

  3. 应用程序类加载器: Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。

双亲委派模型

🌈 拓展一下:

  • rt.jar:rt 代表“RunTime”,rt.jar是 Java 基础类库,包含 Java doc 里面看到的所有的类的类文件。也就是说,我们常用内置库 java.xxx.*都在里面,比如java.util.*、java.io.*、java.nio.*、java.lang.*、java.sql.*、java.math.*。
  • Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。

双亲委派机制

定义

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终会传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子类加载器才会尝试自己去加载该类。

这种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。

在这里插入图片描述

为什么要使用?

  1. 防止内存中出现多份同样的字节码。如果没有该机制而是由各个类加载器自行加载的话,用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都能加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证,同时,也会给虚拟机的安全带来隐患。
  2. 双亲委派机制能够保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同
  3. 这样可以保证系统库优先加载,即便是自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载,从而保证安全性

注意 ⚠️:双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。如果我们因为某些特殊需求想要打破双亲委派模型

执行流程

双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoaderloadClass() 中,相关代码如下所示。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,检查该类是否已经加载过
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果 c 为 null,则说明该类没有被加载过
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //当父类的加载器不为空,则通过父类的loadClass来加载该类
                    c = parent.loadClass(name, false);
                } else {
                    //当父类的加载器为空,则调用启动类加载器来加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父类的类加载器无法找到相应的类,则抛出异常
            }

            if (c == null) {
                //当父类加载器无法加载时,则调用findClass方法来加载该类
                //用户可通过覆写该方法,来自定义类加载器
                long t1 = System.nanoTime();
                c = findClass(name);

                //用于统计类加载器相关的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //对类进行link操作
            resolveClass(c);
        }
        return c;
    }
}

自定义类加载器

构造自定义加载器,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

不破坏双亲委派

实现自定义类加载器的实现,主要分三个步骤

  • 创建一个类继承ClassLoader抽象类
  • 重写findClass()方法
  • 在findClass()方法中调用defineClass()
public class MyClassLoader extends ClassLoader {

    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = getClassBytes(name);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }
  
    private byte[] getClassBytes(String name) throws Exception {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }
}

破坏双亲委派

定义CoderWorldClass.java文件,并使用javac编译成.class文件

public class CoderWorldClass {
  public CoderWorldClass(){
     System.out.println("CoderWorldClass:"+getClass().getClassLoader());
     System.out.println("CoderWorldClass Parent:"+getClass().getClassLoader().getParent());
  }
  public String print(){
    System.out.println("CoderWorldClass method for print NEW");  //修改了打印语句,用来区分被加载的类
    return "CoderWorldClass.print()";
  }
}

在MyClassLoader类中,重写loadClass方法,代码如下

@Override
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) {
      // If still not found, then invoke findClass in order
      // to find the class.
      long t1 = System.nanoTime();

      //非自定义的类还是走双亲委派加载
      if (!name.equals("CoderWorldClass")) { 
        c = this.getParent().loadClass(name);
      } else { //自己写的类,走自己的类加载器。
        c = findClass(name);
      }
      // this is the defining class loader; record the stats
      sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
      sun.misc.PerfCounter.getFindClasses().increment();
    }
    if (resolve) {
      resolveClass(c);
    }
    return c;
  }
}

通过重写loadClass方法,使得自己创建的类,让第一个加载器直接加载,不委托父加载器寻找,从而实现双亲委派的破坏

Tomcat类加载

Tomcat是如何实现应用jar包的隔离的?

在思考这个问题之前,我们先来想想Tomcat作为一个JSP/Servlet容器,它应该要解决什么问题?

  1. 一个web容器需要部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离
  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,必然会带来内存消耗过高的问题。
  3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开

Tomcat类加载机制

认识Tomcat中的类加载器- whvixd | blog

  • Common类加载器作为 Catalina类加载器Shared类加载器 的父加载器。Common类加载器 能加载的类都可以被 Catalina类加载器Shared类加载器 使用。因此,Common类加载器 是为了实现公共类库(可以被所有 Web 应用和 Tomcat 内部组件使用的类库)的共享和隔离。

  • Catalina类加载器Shared类加载器 能加载的类则与对方相互隔离。Catalina类加载器 用于加载 Tomcat 自身的类,为了隔离 Tomcat 本身的类和 Web 应用的类。

  • Shared类加载器 作为 WebApp类加载器 的父加载器,专门来加载 Web 应用之间共享的类比如 Spring、Mybatis。

  • 每个 Web 应用都会创建一个单独的 WebApp类加载器,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 WebAppClassLoader,各个 WebAppClassLoader 实例之间相互隔离,进而实现 Web 应用之间的类隔。

Tomcat如何破坏双亲委派?

假设 Tomcat 服务器加载了一个 Spring Jar 包。项目中用到的基础类库,由于其是 Web 应用之间共享的,因此会由 SharedClassLoader 加载。项目中一些用到了 Spring 的业务类,比如实现了 Spring 提供的接口、用到了 Spring 提供的注解。所以,加载 Spring 的类加载器(也就是 SharedClassLoader)也会用来加载这些业务类。但是业务类在 Web 应用目录下,不在 SharedClassLoader 的加载路径下,所以 SharedClassLoader 无法找到业务类,也就无法加载它们。

如何解决这个问题呢?

这个时候就需要用到 线程上下文类加载器(ThreadContextClassLoader

当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。还记得我上面说的吗?每个 Web 应用都会创建一个单独的 WebAppClassLoader,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 WebAppClassLoader。这样就可以让高层的类加载器(SharedClassLoader)借助子类加载器( WebAppClassLoader)来加载业务类,从而破坏了 Java 类加载的双亲委托机制。


参考内容

十分钟搞懂Java类加载

Java 类加载机制

类加载器详解(重点)

类加载器

请你简单说一下类加载机制的实现原理?

与面试官:如何打破双亲委派机制?相似的内容:

面试官:如何打破双亲委派机制?

面试连环call:1. 双亲委派机制是什么?如何打破双亲委派机制?2. JVM都有哪些类加载器?3. 如何构造一个自定义类加载器?

[转帖]使用Linux命令快速查看某一行

原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。 简介# 当年,我还是Linux菜鸟的时候,就在简历上写着精通Linux命令了,而当面试官问我“如何快速查看某个文件的第200行,仅这一行”时,我的心里是懵逼的。 当时面试官心里估计在窃喜,从我敷衍的回答中,他应该已经完全

多线程知识:三个线程如何交替打印ABC循环100次

本文博主给大家讲解一道网上非常经典的多线程面试题目。关于三个线程如何交替打印ABC循环100次的问题。 > 下文实现代码都基于Java代码在单个JVM内实现。 ## 问题描述 给定三个线程,分别命名为A、B、C,要求这三个线程按照顺序交替打印ABC,每个字母打印100次,最终输出结果为: ``` A

[转帖]浪潮信息龙蜥联合实验室:打造下一代操作系统,筑牢基础软件创新根基

https://aijishu.com/a/1060000000379951 毫无疑问,在当今世界日益复杂的竞争环境下,随着中国企业数字化转型的加速和自主创新进程的加快,再加上开源和云计算已成为行业发展的大趋势之下,如何在“变局中开新局”?如何解决中国在核心基础软件领域面临的各种挑战和难题,可以说已

探索Java通信面试的奥秘:揭秘IO模型、选择器和网络协议,了解面试中的必备知识点!

通过深入探索Java通信面试的奥秘,我们将揭秘Java中的三种I/O模型(BIO、NIO和AIO)、选择器(select、poll和epoll)以及网络协议(如HTTP和HTTPS),帮助您了解在面试中必备的知识点。这些知识点对于网络编程和系统安全方面的求职者来说至关重要,掌握它们将为您的职业发展打下坚实的基础!

HDC2022的无障碍参会体验,手语服务是如何做到的?

华为开发者大会2022(HDC)上,HMS Core手语数字人以全新形象亮相,并在直播中完成了长达3个多小时的实时手语翻译,向线上线下超过一千万的观众提供了专业、实时、准确的手语翻译服务,为听障人士提供了无障碍参会体验。面对专业性强且词汇量大的科技大会,HMS Core手语数字人是如何准确且流畅地打

构建万物互联,华为云IoT+鸿蒙重燃物体感知

摘要:鸿蒙的出现,让硬件、软件行业面临着变革与重构的洪流,但激流勇进中,也潜藏着巨大机遇。物联网设备与鸿蒙结合成为必然趋势,本文将解读华为云IoT+鸿蒙如何强强联合,为物联网行业提供新的思路和方法。 本文分享自华为云社区《华为云IoT携同鸿蒙打造万物智联新机遇》,作者:华为云IoT DTSE团队。

数据智能加持下,中小微企业告别“融资难”

摘要:数据智能是一个跨学科的研究领域,它结合大规模数据处理、数据挖掘、机器学习、可视化等技术,为基于数据制定决策或执行任务提供有效智能支持。面对数据智能蓝海,开发者该如何发力呢?赛道参与者该如何走向成功呢? 本文分享自华为云社区《让数据应用更智能!华为云助力开发者重庆誉存打造数字金融综合解决方案》,

迷茫中翻滚

最近真的是忙疯了,面对着裁员和失业浪潮,我和别人走的是不同的路线,同事们其实已经被市场打脸到不行!什么情怀 人文关怀,企业文化,在现实面前都不值得一击。但是我总是无数次问我自己,当初入行的时候你的初心是什么?成为一个技术超群的程序猿,后来呢?我想着给自己构建了一个蓝图,我要在一家企业一直成长五年,直

给程序员的10堂写作课(一)--30+的你,怎能错过写作?

你是否在职场打拼多年,却始终原地踏步,为升职无望而焦虑?你是否在风云变幻的环境中,整日提心吊胆,不知道何时会被裁员?你是否正步入 35+的行列,面临年龄危机,心生恐惧?如果你正被这些问题所困扰,又苦无对策,那么不妨尝试写作吧。