Java面试宝典专栏范围:JAVA基础,面向对象编程(OOP),异常处理,集合框架,Java I/O,多线程编程,设计模式,网络编程,框架和工具等全方位面试题详解
每日更新Java面试宝典专栏:Java面试宝典
感兴趣的可以先收藏起来
,大家在遇到JAVA面试题等相关问题都可以给我留言咨询
,希望帮助更多的人
回答重点
双亲委派模型是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运行时环境的稳定性,也为开发者提供了一个更安全可靠的类加载框架。