JVM 类加载器之间存在一种层次关系,通常被称为双亲委派模型 (Parent Delegation Model)。这种层次关系和委托机制是 Java 类加载机制的核心,对于保证 Java 程序的安全性和避免类冲突至关重要。
1. 类加载器的层次关系:
JVM 中的类加载器(ClassLoader)主要分为以下几种,它们之间存在自顶向下的层次关系(父子关系,但不是继承关系,而是组合关系):
-
启动类加载器 (Bootstrap Class Loader):
- 最顶层的类加载器。
- 负责加载 Java 核心类库 ,例如
java.lang
、java.util
、java.io
等包中的类(通常位于<JAVA_HOME>/jre/lib
目录下的rt.jar
、resources.jar
等)。 - 用 C/C++ 实现,是 JVM 的一部分,不是 Java 类。
- 没有父类加载器。
- 无法通过 Java 代码直接获取到启动类加载器。
-
扩展类加载器 (Extension Class Loader):
- 父类加载器是启动类加载器。
- 负责加载 Java 扩展类库 (通常位于
<JAVA_HOME>/jre/lib/ext
目录下的 jar 包,或者由java.ext.dirs
系统属性指定的目录)。 - 是
sun.misc.Launcher$ExtClassLoader
的实例(Java 类)。
-
应用程序类加载器 (Application Class Loader / System Class Loader):
- 父类加载器是扩展类加载器。
- 负责加载应用程序的类(classpath 下的类,包括你写的代码、第三方库等)。
- 是
sun.misc.Launcher$AppClassLoader
的实例(Java 类)。 - 通常是默认的类加载器。 可以通过
ClassLoader.getSystemClassLoader()
获取。
-
自定义类加载器 (User-Defined Class Loader):
- 父类加载器通常是应用程序类加载器(也可以是其他自定义类加载器)。
- 由开发者自定义,继承
java.lang.ClassLoader
类。 - 用于实现特殊的类加载逻辑 ,例如:
- 从网络加载类。
- 从数据库加载类。
- 对类进行加密和解密。
- 实现热部署(HotSwap)。
- 实现模块化(OSGi)。
类加载器层次关系图示:
+-----------------------------+
| Bootstrap Class Loader | (C/C++)
+-----------------------------+
↑
| (Parent)
|
+-----------------------------+
| Extension Class Loader | (sun.misc.Launcher$ExtClassLoader)
+-----------------------------+
↑
| (Parent)
|
+-----------------------------+
| Application Class Loader | (sun.misc.Launcher$AppClassLoader)
+-----------------------------+
↑
| (Parent)
|
+-----------------------------+
| User-Defined Class Loader | (extends java.lang.ClassLoader)
+-----------------------------+
2. 双亲委派模型 (Parent Delegation Model):
-
工作原理:
- 当一个类加载器需要加载类时,它首先不会自己尝试加载,而是 委托给它的父类加载器 去加载。
- 父类加载器会检查自己是否已经加载过该类。如果已经加载,则直接返回
Class
对象。 - 如果父类加载器没有加载过该类,则会尝试加载。如果父类加载器在其搜索范围内找到了该类,则加载成功,并返回
Class
对象。 - 如果父类加载器无法加载该类(在其搜索范围内找不到该类),则 将加载请求返回给子类加载器。
- 子类加载器尝试加载该类。如果子类加载器能够加载,则加载成功,并返回
Class
对象。 - 如果子类加载器也无法加载该类,则抛出
ClassNotFoundException
异常。
-
特例: 启动类加载器没有父类加载器,它会直接尝试加载。
-
代码示例 (简化版):
java// java.lang.ClassLoader 中的 loadClass 方法 (简化版) 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. 如果没有父类加载器 (到达了启动类加载器),则调用 findBootstrapClassOrNull c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 父类加载器无法加载该类 } if (c == null) { // 4. 如果父类加载器无法加载,则调用 findClass 方法尝试自己加载 c = findClass(name); } } if (resolve) { resolveClass(c); // 链接类 (可选) } return c; } }
findClass()
方法是需要子类加载器重写的方法, 用于实现自定义的类加载逻辑.
3. 双亲委派模型的优点:
- 避免类的重复加载: 同一个类只会被加载一次,避免了命名冲突和资源浪费。
- 保证 Java 核心类库的安全性: 用户自定义的类加载器无法加载或替换 Java 核心类库中的类(例如
java.lang.Object
、java.lang.String
),因为核心类库总是由启动类加载器加载。这可以防止恶意代码篡改核心类库,破坏 JVM 的安全性。 - 命名空间隔离: 不同的类加载器加载的类位于不同的命名空间, 可以防止类名冲突.
4. 破坏双亲委派模型:
虽然双亲委派模型有很多优点,但在某些情况下,可能需要破坏双亲委派模型,例如:
-
JDBC、JNDI、JAXP 等 SPI (Service Provider Interface) 机制:
- 这些 API 的核心接口是由启动类加载器加载的,但具体的实现类通常由应用程序类加载器或自定义类加载器加载。
- 为了解决这个问题,Java 引入了线程上下文类加载器 (Thread Context ClassLoader)。
- 可以通过
Thread.currentThread().getContextClassLoader()
获取线程上下文类加载器。 - 例如,JDBC 驱动程序通常由应用程序类加载器加载,但 JDBC API (如
java.sql.DriverManager
) 需要能够加载这些驱动程序。DriverManager
会使用线程上下文类加载器来加载驱动程序。
-
OSGi (Open Service Gateway initiative):
- OSGi 是一个模块化系统,每个模块(bundle)都有自己的类加载器。
- OSGi 的类加载器模型不是严格的双亲委派模型,而是更复杂的网状结构。
-
热部署 (HotSwap):
- 在应用程序运行时,动态地替换或更新类。
- 通常需要自定义类加载器,并破坏双亲委派模型。
-
Tomcat:
- Tomcat 为了实现 web 应用之间的隔离,以及共享库的加载,破坏了双亲委派模型。
- 每个 web 应用都有自己的类加载器 (WebAppClassLoader)。
总结:
JVM 类加载器之间存在层次关系(启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器),并通过双亲委派模型协同工作。双亲委派模型保证了类加载的顺序和安全性,避免了类的重复加载,并防止了核心类库被篡改。 在某些特殊情况下,可能需要破坏双亲委派模型(例如,SPI、OSGi、热部署)。