Java 的双亲委派

1. 双亲委派机制

1. 什么是类加载器

类加载器(Class loaders), 是 JRE 的一部分,负责在 JVM 程序运行时,动态加载 Java 类到 JVM 内存中,通过类加载器,JVM 在运行 JAVA 程序时,不需要知道底层的文件系统和文件,即类加载器解耦了 JVM 和操作系统底层的文件系统。之所以说类是 “动态” 加载的,主要是因为运行程序时需要的所有的 Java 类,并不是一次性全部加载到 JVM 内存中,而是在需要哪个类时才加载那个类。

2. 有哪些类加载器

Java 语言系统中支持以下 4 种类加载器,从顶到底(父子级关系)分别是:

1. Bootstrap ClassLoader

  • 启动类加载器,是 JVM 核心的一部分,主要负责加载 Java 核心类库,位于 %JRE_HOME%\lib 下的rt.jarresources.jarcharsets.jar
  • 它是所有其它类加载器的父类,不是普通类继承关系层面上的父类
  • 它是使用 native 代码编写的,所以在不同的平台上(如windows,linus,macos),有不同的实现,也因此打印时会显示为 null

2. Extention ClassLoader

  • 标准扩展类加载器,主要负责加载 JDK 的扩展类,位于 %JRE_HOME%\lib\ext 目录下的 jar 包和 class 文件,或系统变量 java.ext.dirs 指定的其它目录
  • 它是 Bootstrap ClassLoader 的子类,一般显示为 sun.misc.Launcher$ExtClassLoader@XXXXX

3. Application ClassLoader

  • 应用类加载器,主要负责加载当前应用的环境变量 classpath 下的所有类,可以通过命令行参数 -classpath-cp 指定环境变量 classpath
  • 它是 Extention ClassLoader 的子类,一般显示为 sun.misc.Launcher$AppClassLoader@XXXXX

4. User ClassLoader

用户自定义类加载器

3. 什么是双亲委派机制

  • 双亲,更严谨来讲,其实是多级单亲,所有的类加载器,除 Bootstrap ClassLoader,都有一个唯一的父加载器,而所说的父子关系,不是普通类层面上的父子继承关系 (extends),而更像是一种有层次之分的组合关系
  • 当类加载器收到一个类或资源的加载请求后,会首先搜索是否已被加载到内存中,如果没有,它会把这个请求委托给自己的父加载器去加载
  • 父级加载器一样遵循向上委派机制,将加载请求委派给其父级加载器去加载,通过这种向上传导关系,所有的类加载请求,最终都会被传入到启动类加载器 Bootstrap ClassLoader
  • 当当父级加载器反馈无法完成特定类或资源的加载请求时,即父级加载器在它的搜索范围内找不到该类或资源时,子级加载器才尝试在自己的搜索范围内加载类或资源
  • 如果所有父子加载器在自己的搜索范围内都找不到该类或资源,JVM 就会报 java.lang.ClassNotFoundException

4. 双亲委派的优点

通过以上双亲委派模型,JVM 确保了类的行为:

  • 唯一性(Unique):只有在父类加载器在其搜索范围内找不到类,无法加载类时,子类加载器才会尝试自己加载,这样就避免了重复加载,确保了类的唯一性
  • 可见性(Visibility):父类加载器加载的类对子类加载器可见(即能访问),而子类加载器加载的类对父类加载器不可见
  • 安全性(Secure):双亲委派模型实现了类的唯一性和可见性,避免了用户随意定义类加载器加载核心 API 带来的安全隐患,从而进一步确保了类的安全性;

5. 双亲委派的缺点

JVM 核心类可能需要动态加载应用开发人员提供的特定类或资源,如 JNDI 或 JDBC ,它们核心功能都是在 rt.jar 的核心内部类中实现的,但在程序运行时,这些类可能需要加载路径下各个具体实现类(通过命令行参数 -classpath-cp 指定类加载路径的具体目录),此时就需要 Bootstrap ClassLoader 加载本应只对 Application ClassLoader 可见的类。

此时,双亲委派机制就不能满足了,为此,JVM 提供了 Thread ContextClassLoader

  • 每个线程在创建时,都有一个对应的 ContextClassLoader,来负责该线程中类或资源的加载
  • 当没有显示指定时,线程的 ContextClassLoader 跟其父线程的 ContextClassLoader 是同一个
  • 可以通过方法 getContextClassLoader() 获得当前线程的 ContextClassLoader
  • 可以通过方法 setContextClassLoader(ClassLoader cl) 重置当前线程的 ContextClassLoader

2. 为什么需要双亲委派

因为类加载器之间有严格的层次关系,那么得 Java 类也随之具备了层次关系,即优先级。

一个用户自定义的类,他会被一直委托到 Bootstrap ClassLoader,但是因为 Bootstrap ClassLoader 不负责加载该类,那么会在由 Extention ClassLoader 尝试加载,而 Extention ClassLoader 也不负责这个类的加载,最终才会被 Application ClassLoader 加载。

这种机制有几个好处:

  • 可以避免类的重复加载:当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类
  • 保证了安全性:因为 Bootstrap ClassLoader 在加载的时候,只会加载 JAVA_HOME 中的 jar 包里面的类,如 java.lang.Integer,那么这个类是不会被随意替换的,可以有效的防止核心 Java API 被篡改,除非 JDK 被破坏

3. "父子加载器"之间的关系是继承吗

双亲委派模型中,类加载器之间的父子关系一般 不会以继承(Inheritance) 的关系来实现,而是 使用组合(Composition) 关系来复用父加载器的代码的。

ClassLoader 中父加载器的定义:

1
2
3
4
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
}

4. 双亲委派是怎么实现的

实现双亲委派的代码都集中在 java.lang.ClassLoaderloadClass() 方法之中,主要就是以下几个步骤:

  1. 先检查类是否已经被加载过
  2. 若没有加载则调用父加载器的 loadClass() 方法进行加载
  3. 若父加载器为空则默认使用启动类加载器作为父加载器
  4. 如果父类加载失败,抛出 ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);

if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}

} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}

if (resolve) {
resolveClass(c);
}

return c;
}
}

5. 如何主动破坏双亲委派机制

由于双亲委派过程都是在 loadClass 方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器,重写其中的 loadClass 方法,使其不进行双亲委派即可。

6. loadClass()、findClass()、defineClass() 区别

  • loadClass():就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。
  • findClass():根据名称或位置加载 .class 字节码
  • definclass():把字节码转化为 Class

我们想定义一个类加载器,但是不想破坏双亲委派模型时,可以继承 ClassLoader ,并且重写 findClass() 方法。该方法只抛出了一个异常,没有默认实现:

1
2
3
4
5
6
/**
* @since 1.2
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

JDK1.2 之后不再提倡用户直接覆盖 loadClass() 方法,而是建议把自己的类加载逻辑实现到 findClass() 方法中。因为在 loadClass() 方法的中,如果父类加载器加载失败,则会调用自己的 findClass() 方法来完成加载。

7. 双亲委派被破坏的例子

  1. 被破坏的情况是在双亲委派出现之前。比如在 JDK1.2 之前实现的自定义类加载器。
  2. JNDI、JDBC 等需要加载 SPI 接口实现类的情况。
  3. 为了实现热插拔热部署工具。为了让代码动态生效而无需重启,实现方式时把模块连同类加载器一起换掉就实现了代码的热替换。
  4. Tomcat 等 web 容器的出现。
  5. OSGI、Jigsaw 等模块化技术的应用。

8. 为什么 JNDI,JDBC 等需要破坏双亲委派

大多数时候会通过 API 的方式调用 Java 提供的那些基础类,这些基础类时被 Bootstrap 加载的。但是,还有一种 SPI 的方式,比如 JDBC 服务:

1
2
Connection conn = DriverManager
.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234");

DriverManager 会先被类加载器加载,因为 java.sql.DriverManager 类是位于 rt.jar 下面的 ,所以他会被根加载器加载。

类加载时,会执行该类的静态方法。其中有一段关键的代码是:

1
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

这段代码,会尝试加载 classpath 下面的所有实现了 Driver 接口的实现类。

问题是,DriverManager 是被根加载器加载的,那么在加载时遇到以上代码,会尝试加载所有 Driver 的实现类,但是这些实现类基本都是第三方提供的,根据双亲委派原则,第三方的类不能被根加载器加载。于是,在 JDBC 中通过引入 ThreadContextClassLoader(线程上下文加载器,默认情况下是 AppClassLoader)的方式破坏了双亲委派原则。

我们深入到ServiceLoader.load方法就可以看到:

1
2
3
4
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

第一行,获取当前线程的线程上下⽂类加载器 AppClassLoader,⽤于加载 classpath 中的具体实现类。

9. 为什么 Tomcat 要破坏双亲委派

一个 web 容器可能需要部署多个应用程序。不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。

如多个应用都要依赖 hello.jar,但是 A 应用需要依赖 1.0.0 版本,但是 B 应用需要依赖 1.0.1 版本。这两个版本中都有一个类是 com.hello.Test.class。如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。

所以,Tomcat 破坏双亲委派原则,提供隔离的机制,为每个 web 容器单独提供一个 WebAppClassLoader 加载器。

Tomcat 的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器—— WebAppClassLoader,负责加载本身的目录下的 class 文件,加载不到时再交给 CommonClassLoader 加载,这和双亲委派刚好相反。

10. 模块化技术与类加载机制

OSGI 框架是模块化的,而之所以能够实现模块热插拔和模块内部可见性的精准控制都归结于其特殊的类加载机制,加载器之间的关系不再是双亲委派模型的树状结构,而是发展成复杂的网状结构。

在 JDK 中,双亲委派也不是绝对的了。在 JDK9 之前,JVM 的基础类以前都是在 rt.jar 这个包里,这个包也是 JRE 运行的基石。这不仅是违反了单一职责原则,同样程序在编译的时候会将很多无用的类也一并打包,造成臃肿。

在 JDK9 中,整个 JDK 都基于模块化进行构建,以前的 rt.jartool.jar 被拆分成数十个模块,编译的时候只编译实际用到的模块,同时各个类加载器各司其职,只加载自己负责的模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Class<?> c = findLoadedClass(cn);

if (c == null) {
// 找到当前类属于哪个模块
LoadedModule loadedModule = findLoadedModule(cn);

if (loadedModule != null) {
//获取当前模块的类加载器
BuiltinClassLoader loader = loadedModule.loader();

//进行类加载
c = findClassInModuleOrNull(loadedModule, cn);
} else {
// 找不到模块信息才会进行双亲委派
if (parent != null) {
c = parent.loadClassOrNull(cn);
}
}
}