回答重点:
双亲委派模型是Java类加载机制的设计模式之一 。它的核心思想是:类加载器在加载某个类时,会先委派给父类加载器去加载,父类加载器无法加载时,才由当前类加载器自行去加载。
工作流程:
当前类加载器收到一个类加载的请求
当前类加载器会将这个请求委托给它的父类加载器去加载
父类加载器再将请求向上继续委派,直到达到Bootstrap类加载器
如果Bootstrap类加载器无法加载目标类(即不在其加载范围内),加载请求回到下一级(即扩展类加载器),由它尝试加载类
如果到达当前类加载器仍然无法加载,则抛ClassNotFountExeption异常
详细解释
三种类加载器
Java 中的类加载器分为以下三类:
启动类加载器(Bootstrap ClassLoader)
职责:负责加载 JVM 的核心类库。
加载范围:
<JAVA_HOME>/lib 目录,例如 rt.jar。
被 -Xbootclasspath 指定的路径下的类。
实现方式:由 C++ 实现,是 JVM 的一部分。
扩展类加载器(Extension ClassLoader)
职责:加载 JVM 扩展功能的类库。
加载范围:
<JAVA_HOME>/lib/ext 目录。
系统属性 java.ext.dirs 指定的路径下的类。
实现方式:由 java.lang.ClassLoader 继承实现。
应用程序类加载器(Application ClassLoader)
职责:加载用户定义类路径(classpath)下的类。
加载范围:
开发者编写的类和资源文件。
特性:是 Java 中默认的类加载器。若未自定义类加载器,则所有类将由此加载。
为什么要有双亲委派机制?
避免类的重复加载: 确保同一个类不会被重复加载。当某个类已经被加载到内存中时(例如基础类库中的java.lang.Object,或系统中已有的一些核心类),其他类加载器如果再想加载该类,就会通过双亲委派机制交给已有的类加载器处理,避免重复加载。
提高安全性: 通过将核心类交由顶层类加载器(如Bootstrap ClassLoader)加载,防止核心API被篡改。这样任何用户自定义的类加载器都无法替代Java的核心类,保护了运行时环境的完整性和安全性。
保证一致性: Java提供的核心类库(Java API)在任何环境下都应该是一致的,通过双亲委派机制,确保了无论在什么自定义类加载器下,核心类库总是由系统的父类加载器来加载的,这保证了Java应用程序在不同环境下的一致行为。
简化系统架构: 双亲委派模型简化了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 文件的功能,通过覆写 ClassLoader 的 findClass 方法,动态读取类文件并将字节数据转换为 Java 的 Class 对象。构造函数接受类路径,findClass 方法处理类名转路径的转换并加载数据,而 loadClassData 方法则负责读取文件内容。主方法演示了如何使用该类加载器加载并实例化特定类,此设计适用于动态扩展应用功能及处理特殊环境下的类加载需求。
双亲委派机制先自下而上委托,再自上而下加载,那为什么不直接自上而下加载?
双亲委派机制的自上而下加载策略旨在维护Java类加载的安全性和一致性,通过优先委托给父类加载器先加载类,避免了同名类在多个加载器中引发的冲突与不一致性,确保系统核心类(如 java.lang 包)不被恶意修改,从而有效防止类篡改和安全漏洞。此外,该机制通过重用父加载器已加载的类,提高了性能,遵循单一职责原则,从而降低了系统的复杂性与耦合性。这一设计不仅增强了Java运行时环境的稳定性,也为开发者提供了一个更安全可靠的类加载框架。
0条评论
点击登录参与评论