引言
在 Java 中,类加载器(ClassLoader)是 JVM 的重要组成部分,负责将类的字节码加载到内存中并生成对应的 Class
对象。Java 的类加载器采用了一种称为 双亲委派机制(Parent Delegation Model) 的设计模式,这种机制不仅保证了类的唯一性,还增强了 Java 程序的安全性和稳定性。本文将深入探讨双亲委派机制的原理、实现及其重要性。
1. 什么是双亲委派机制?
双亲委派机制是 Java 类加载器的一种工作模式,其核心思想是:
当一个类加载器需要加载某个类时,它首先会委托其父类加载器去加载,只有在父类加载器无法加载时,才会尝试自己加载。
这种机制类似于“先问长辈,再自己动手”的逻辑,确保了类的加载过程是有序且安全的。
2. 类加载器的层次结构
在 Java 中,类加载器分为以下几类,它们之间存在层次关系:
-
Bootstrap ClassLoader(启动类加载器):
-
最顶层的类加载器,由 C/C++ 实现,负责加载 JVM 核心类库(如
java.lang.*
、java.util.*
等)。 -
没有父类加载器。
-
-
Extension ClassLoader(扩展类加载器):
-
负责加载
JAVA_HOME/lib/ext
目录下的类库,或由java.ext.dirs
系统变量指定的路径。 -
父类加载器是 Bootstrap ClassLoader。
-
-
Application ClassLoader(应用程序类加载器):
-
也称为系统类加载器(System ClassLoader),负责加载用户类路径(ClassPath)下的类。
-
父类加载器是 Extension ClassLoader。
-
-
自定义类加载器:
-
开发者可以继承
ClassLoader
类,实现自定义的类加载逻辑。 -
父类加载器是 Application ClassLoader。
-
3. 双亲委派机制的工作流程
当一个类加载器收到加载类的请求时,它会按照以下步骤处理:
-
委托父类加载器:
-
首先检查是否已经加载过该类,如果已加载则直接返回。
-
如果没有加载过,则将加载请求委托给父类加载器。
-
-
父类加载器尝试加载:
-
父类加载器重复同样的过程,继续向上委托,直到到达 Bootstrap ClassLoader。
-
-
自上而下尝试加载:
-
如果父类加载器无法加载该类(例如在指定路径下找不到类文件),则由子类加载器尝试加载。
-
-
加载成功或抛出异常:
-
如果某个类加载器成功加载了类,则返回对应的
Class
对象。 -
如果所有类加载器都无法加载该类,则抛出
ClassNotFoundException
。
-
4. 双亲委派机制的代码实现
以下是 ClassLoader
类中双亲委派机制的核心代码(简化版):
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 1. 检查是否已经加载过该类Class<?> c = findLoadedClass(name);if (c == null) {try {// 2. 委托父类加载器加载if (parent != null) {c = parent.loadClass(name, false);} else {// 3. 如果父类加载器为 null,则委托 Bootstrap ClassLoaderc = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 父类加载器无法加载,忽略异常}// 4. 如果父类加载器未加载,则由当前类加载器加载if (c == null) {c = findClass(name);}}// 5. 如果需要解析,则解析该类if (resolve) {resolveClass(c);}return c;}
}
5. 双亲委派机制的优势
1. 避免类的重复加载
-
通过委派机制,确保每个类只被加载一次,避免了类的重复加载和内存浪费。
2. 保证核心类库的安全性
-
核心类库(如
java.lang.*
)由 Bootstrap ClassLoader 加载,避免了用户自定义类替换核心类的情况,增强了安全性。
3. 保证类的唯一性
-
同一个类在不同的类加载器中加载会生成不同的
Class
对象,双亲委派机制确保了类的唯一性。
6. 打破双亲委派机制的场景
虽然双亲委派机制是 Java 类加载的默认行为,但在某些场景下需要打破这种机制:
-
SPI(Service Provider Interface):
-
例如 JDBC 的
DriverManager
,需要加载由第三方实现的驱动类,这些类由 Bootstrap ClassLoader 加载,但 Bootstrap ClassLoader 无法直接加载用户类路径下的类,因此需要打破双亲委派机制。
-
-
热部署:
-
在某些应用服务器(如 Tomcat)中,为了实现热部署,需要为每个 Web 应用提供独立的类加载器。
-
-
OSGi 模块化:
-
OSGi 框架采用了一种更灵活的类加载机制,允许模块之间的类加载器相互协作。
-
案例
package java.lang;
public class String {
public String toString(){
return "Hello";
}
public static void main(String[] args){
String s= new String();
s.toString();
这段代码为什么报错?
1. 双亲委派机制回顾
在 Java 中,类加载器采用双亲委派机制来加载类。其核心规则是:
-
当一个类加载器收到加载类的请求时,它首先会委托其父类加载器去加载。
-
只有当父类加载器无法加载时,子类加载器才会尝试自己加载。
Java 的核心类库(如 java.lang.String
)是由 Bootstrap ClassLoader 加载的,而用户自定义的类通常由 Application ClassLoader 加载。
2. 代码问题分析
问题 1:包名和类名冲突
package java.lang;
public class String {
// 其他代码
}
-
问题:你定义了一个
java.lang.String
类,这与 Java 核心类库中的java.lang.String
类完全冲突。 -
双亲委派机制的影响:
-
当 JVM 尝试加载
java.lang.String
时,会首先委托 Bootstrap ClassLoader 去加载。 -
Bootstrap ClassLoader 会加载核心类库中的
java.lang.String
,而不是你自定义的String
类。 -
因此,你的
String
类永远不会被加载,导致后续代码无法正常运行。
-
问题 2:main 方法中的错误
String s = new String();
s.toString();
-
问题:
-
这里的
String
是核心类库中的java.lang.String
,而不是你自定义的String
类。 -
由于双亲委派机制,JVM 加载的是核心类库中的
String
,而不是你定义的String
。 -
因此,
s.toString()
调用的是核心类库中String
类的toString
方法,而不是你自定义的toString
方法。
-
3. 双亲委派机制的具体体现
-
类加载过程:
-
当 JVM 遇到
String s = new String();
时,会尝试加载java.lang.String
类。 -
根据双亲委派机制,加载请求会首先委托给 Bootstrap ClassLoader。
-
Bootstrap ClassLoader 会加载核心类库中的
java.lang.String
,而不是你自定义的String
类。
-
-
结果:
-
你自定义的
String
类永远不会被加载,因此其中的toString
方法也不会被调用。 -
最终,
s.toString()
调用的是核心类库中String
类的toString
方法,而不是你期望的自定义方法。
-
-
双亲委派机制 确保了核心类库的安全性,避免了用户自定义类覆盖核心类库的情况。
-
不要尝试在
java.lang
包下定义类,尤其是与核心类库同名的类(如String
、Integer
等)。 -
遵循 Java 的命名规范,避免与核心类库冲突。
7. 总结
双亲委派机制是 Java 类加载器的核心设计,它通过层次化的类加载器结构和委派机制,保证了类的唯一性、安全性和稳定性。理解双亲委派机制不仅有助于我们更好地掌握 Java 的类加载过程,还能帮助我们在实际开发中解决类加载相关的问题。