Bootstrap

Java面试宝典:什么是Java中的双亲委派模型?

Java面试宝典专栏范围:JAVA基础,面向对象编程(OOP),异常处理,集合框架,Java I/O,多线程编程,设计模式,网络编程,框架和工具等全方位面试题详解
每日更新Java面试宝典专栏:Java面试宝典
感兴趣的可以先收藏起来,大家在遇到JAVA面试题等相关问题都可以给我留言咨询,希望帮助更多的人

封面图

回答重点

双亲委派模型是Java类加载机制的设计模式之一 。它的核心思想是:类加载器在加载某个类时,会先委派给父类加载器去加载,父类加载器无法加载时,才由当前类加载器自行去加载。

工作流程:

  1. 当前类加载器收到一个类加载的请求
  2. 当前类加载器会将这个请求委托给它的父类加载器去加载
  3. 父类加载器再将请求向上继续委派,直到达到Bootstrap类加载器
  4. 如果Bootstrap类加载器无法加载目标类(即不在其加载范围内),加载请求回到下一级(即扩展类加载器),由它尝试加载类
  5. 如果到达当前类加载器仍然无法加载,则抛ClassNotFountExeption异常

图示

详细解释

三种类加载器

Java 中的类加载器分为以下三类:

  1. 启动类加载器(Bootstrap ClassLoader)

    • 职责:负责加载 JVM 的核心类库。
    • 加载范围:
      • <JAVA_HOME>/lib 目录,例如 rt.jar
      • -Xbootclasspath 指定的路径下的类。
    • 实现方式:由 C++ 实现,是 JVM 的一部分。
  2. 扩展类加载器(Extension ClassLoader)

    • 职责:加载 JVM 扩展功能的类库。
    • 加载范围:
      • <JAVA_HOME>/lib/ext 目录。
      • 系统属性 java.ext.dirs 指定的路径下的类。
    • 实现方式:由 java.lang.ClassLoader 继承实现。
  3. 应用程序类加载器(Application ClassLoader)

    • 职责:加载用户定义类路径(classpath)下的类。
    • 加载范围:
      • 开发者编写的类和资源文件。
    • 特性:是 Java 中默认的类加载器。若未自定义类加载器,则所有类将由此加载。
委托加载
委托加载
委托加载
加载失败后向下传递
加载失败后向下传递
加载失败后向下传递
自定义ClassLoader
Application ClassLoader
Extension ClassLoader
Bootstrap ClassLoader

为什么要有双亲委派机制?

  1. 避免类的重复加载: 确保同一个类不会被重复加载。当某个类已经被加载到内存中时(例如基础类库中的java.lang.Object,或系统中已有的一些核心类),其他类加载器如果再想加载该类,就会通过双亲委派机制交给已有的类加载器处理,避免重复加载。

  2. 提高安全性: 通过将核心类交由顶层类加载器(如Bootstrap ClassLoader)加载,防止核心API被篡改。这样任何用户自定义的类加载器都无法替代Java的核心类,保护了运行时环境的完整性和安全性。

  3. 保证一致性: Java提供的核心类库(Java API)在任何环境下都应该是一致的,通过双亲委派机制,确保了无论在什么自定义类加载器下,核心类库总是由系统的父类加载器来加载的,这保证了Java应用程序在不同环境下的一致行为。

  4. 简化系统架构: 双亲委派模型简化了Java虚拟机对类及其依赖的管理,使类加载器之间形成一种树状结构,使得系统架构更为简明,减少了类加载器之间的复杂交互。

那你知道有违反双亲委派的例子吗?

典型违反双亲委派的例子就是JDBC
JDBC 的接口是类库定义的,但实现在各大数据库厂商提供的 jar 包中,通过启动类加载器找不到这个实现类,所以需要应用程序加载器完成这个任务,这就违反了自下而上的委托机制。

具体做法是搞了个线程上下文类加载器,通过 setContextClassLoader () 默认设置了应用程序类加载器,然后通过 Thread.current.currentThread ().getContextClassLoader () 获得类加载器来加载。

这是一个具体的例子,实际上 Java 的 SPI 机制都违反了双亲委派模型。因为 SPI 允许开发者在类路径中自定义服务实现,通常通过线程上下文类加载器来加载 SPI 实现类,绕过了父类加载器。

除此之外,在 Java EE 容器(如 Tomcat、WebLogic)中,每个 Web 应用有自己的类加载器,应用级别的类加载器优先加载应用的类库,而不是父类加载器。所以它们也违反了双亲委派。

请你自定义一个类加载器?

继承ClassLoader类冲洗额findClass方法即可实现

import java.io.*;

public class CustomClassLoader extends ClassLoader {

    private String classPath;

    // 构造方法,传入类文件路径
    public CustomClassLoader(String classPath) {
        this.classPath = classPath;
    }

    // 重写 findClass 方法
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 将类名转换为路径
            String fileName = classPath + name.replace('.', '/') + ".class";

            // 读取类文件
            byte[] classData = loadClassData(fileName);
            if (classData == null) {
                throw new ClassNotFoundException("Class file for " + name + " not found");
            }

            // 将字节数组转换为 Class 对象
            return defineClass(name, classData, 0, classData.length);
        } catch (IOException e) {
            throw new ClassNotFoundException("Error reading class file for " + name, e);
        }
    }

    // 加载类文件为字节数组
    private byte[] loadClassData(String fileName) throws IOException {
        File file = new File(fileName);
        if (!file.exists()) {
            return null;
        }

        try (InputStream inputStream = new FileInputStream(file);
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }

            return outputStream.toByteArray();
        }
    }

    public static void main(String[] args) {
        try {
            // 指定 .class 文件的路径
            String classPath = "/path/to/classes/";

            // 创建自定义类加载器
            CustomClassLoader customClassLoader = new CustomClassLoader(classPath);

            // 类的全限定名,例如 "com.example.MyClass"
            String className = "com.example.MyClass";

            // 使用自定义类加载器加载类
            Class<?> loadedClass = customClassLoader.loadClass(className);

            // 打印加载的类信息
            System.out.println("Class loaded: " + loadedClass.getName());
            System.out.println("Class loader: " + loadedClass.getClassLoader());

            // 调用类的默认构造方法创建一个实例
            Object instance = loadedClass.getDeclaredConstructor().newInstance();
            System.out.println("Instance created: " + instance);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这这段自定义类加载器代码实现了一个能够从指定路径加载 .class 文件的功能,通过覆写 ClassLoaderfindClass 方法,动态读取类文件并将字节数据转换为 Java 的 Class 对象。构造函数接受类路径,findClass 方法处理类名转路径的转换并加载数据,而 loadClassData 方法则负责读取文件内容。主方法演示了如何使用该类加载器加载并实例化特定类,此设计适用于动态扩展应用功能及处理特殊环境下的类加载需求。

双亲委派机制先自下而上委托,再自上而下加载,那为什么不直接自上而下加载?

双亲委派机制的自上而下加载策略旨在维护Java类加载的安全性和一致性,通过优先委托给父类加载器先加载类,避免了同名类在多个加载器中引发的冲突与不一致性,确保系统核心类(如 java.lang 包)不被恶意修改,从而有效防止类篡改和安全漏洞。此外,该机制通过重用父加载器已加载的类,提高了性能,遵循单一职责原则,从而降低了系统的复杂性与耦合性。这一设计不仅增强了Java运行时环境的稳定性,也为开发者提供了一个更安全可靠的类加载框架。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;