JVM可以使用类加载器通过全限定类名的方式来加载一个类。类加载器在加载类的时候会使用一种称为「双亲委派」的机制来实现类的加载,本文将简要介绍双亲委派机制。

双亲委派机制

类加载器是JVM执行类加载机制的前提,类加载器实现了「双亲委派」机制,介绍「双亲委派」机制前先简要介绍一下类加载器。

一、类加载器

1.1 类加载器作用

1
Xxxx.class -----二进制流-----> Class对象

ClassLoader是Java的核心组件,所有的Class都是ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制流数据读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。因此,ClassLoader在整个装载阶段,只能影响类加载,而无法通过ClassLoader区改变类的链接和初始化行为。至于是否可以运行,有Execution Engine决定。

类加载器最早出现在Java1.0中,那时候只是单纯为了满足Java Applet应用而被研发出来,但如今却在OSGi、字节码加解密中大放异彩。这主要归功于Java虚拟机的设计者当初在设计类加载的时候,并没有考虑将它绑定到JVM内部,这样做的好处是能够灵活和动态的执行类加载操作。

image-20201120221030003

1.2 类加载方式的分类

JVM可以通过显式加载与隐式加载将class文件加载到内存中

  • 显式加载,通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。
  • 隐式加载,通过Java虚拟机自动加载到内存中,如加载某个类时,该类中引用了另外一个类,此时则通过隐式加载。

1.3 类加载的基本机制

类加载的时候有三个特征:

  • 双亲委派模型。不是所有的类加载都遵循这个模型。有时候,启动类加载的类型可能要加载用户代码,如JDK内部的ServiceProvider、ServiceLoader机制,用户可用在标准的API框架上,提供自己的实现,如Java中的JNDI、JDBC、文件系统、Cipher等,都是利用该机制。这时不是采用双亲委派机制,而是上下文加载器。
  • 可见性,子类加载器可用访问父加载器加载的类型,但反过来不行 。不然因为缺少不要的割裂就没办法利用类加载器去实现容器的逻辑。
  • 单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为互不可见。

1.4 类加载器类型

JVM把类加载器分为两类:引导类加载器(Bootstrap ClassLoader)和自定义加载器(User-Defined ClassLoader)。

不同的类加载器主机不是继承关系,而是包含关系,下层类加载器包含上层类加载器的引用

启动类加载器 /引导类加载器

Bootstrap ClassLoader,使用C/C++实现,嵌套在JVM内部。

加载Java核心库:JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path,用于提供JVM自身需要的类。

并不继承与java.lang.ClassLoader,没有父加载器。

出于安全考虑,Bootstrap启动类加载器只加载包名为java/javax/sun等开头的类。

引导类加载器可以加载扩展类加载器和应用程序类加载器,并为他们指定父类加载器。

扩展类加载器

Extension ClassLoader,Java编写,sun.misc.Launcher$ExtClassLoader实现。

Extension ClassLoader继承于ClassLoader,它的父类加载器为启动类加载器。

从java.ext.dirs系统属性指定的目录加载类库,或从jre/lib/ext子目录下加载类库。

如果用户将jar放到以上目录,将会自动被加载。

用户自定义类加载器

日常开发中都是使用上述三种类加载器交叉使用,但必要时我们也可以自定义类加载器。

Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是jar,也可以是网络的远程资源。

通过类加载器可以实现精美绝伦的插件机制。如著名的OSGI组件框架、Eclipse插件机制等。

类加载器为应用程序提供了一种动态添加新功能的机制,这种机制无需重新打包发布应用程序就能实现。

同时,自定义类加载器可以实现应用隔离,如 Tomcat、Spring等中间件和组件框架都在内部实现了自定义加载器,并且通过自定义加载器隔离不同的组件模块。

自定义类加载器通常继承与ClassLoader。

二、双亲委派机制

上面在介绍类加载器时提到,类加载器实现类加载时,具有「双亲委派」的特征,下面正式聊聊双亲委派模型。

JDK1.2开始,类的加载过程采用双亲委派机制,这机制能够更好的保证Java平台安全。

2.1 定义

一个类加载器在接收到类加载的请求的时候,首先不会自己尝试加载,而是先把这请求交给父类加载器去加载,依次递归。如果父类加载器可以完成类的加载任务,则成功返回;否则自己其加载。

双亲委派机制本质上是规定了类的加载顺序:引导类加载器 –> 扩展类加载器 –> 系统类加载器 –> 自定义类加载器

2.2 代码实现

双亲委派机制具体实现是在java.lang.ClassLoader.loadClass(String, boolean)中体现。逻辑如下:

(1)当前类加载器缓存中查找该类,有则直接返回;

(2)判断当前父类加载器是否为空,不为空,则调用parent.loadClass()进行加载;【双亲委派机制】

(3)如果父类加载器为空,则调用findBootstrapClassOrNull()接口,让引导类加载器加载;【双亲委派机制】

(4)如果以上三步都不成功,自己加载,调用findClass(name)。该接口最终调用ClassLoader的defineClass系列的native接口加载目标Java类。

Java 8 java.lang.ClassLoader.loadClass(String, boolean)

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
protected Class<?> loadClass(String name, boolean resolve)          // loadClass
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;
}
}

2.3 实践

Java虚拟机规范没有明确要求类加载器的加载机制一定要采用双亲委派机制,只是建议使用。

在Tomcat中,类加载器采用的加载机制和双亲委派机制有一定区别,当缺省的类加载器收到一个加载任务的时候,首先会自行加载,当加载失败,才会将类加载委派给父类加载器,同时也是Servlet规范推荐的做法。

2.4 破坏双亲委派机制

双亲委派机制不是一个具有强制性的模型,而是Java设计者推荐使用的类加载方式。

在JDK9之前,出现过三次破坏双亲委派机制的情况。

(1)JDK1.2前

ClassLoader诞生于JDK1.0,但是双亲委派机制是JDK1.2才引入。因此JDK1.2 之前的自定义实现的用户自定义类加载器是不具有双亲委派机制的。同时,为了对已有代码的兼容,JDK1.2在对具体类加载器实现时,将类字节码文件的加载以及字节码文件转换为Class文件分离,分别是loadClass与defineClass方法。推荐使用重写defineClass来避免破坏双亲委派机制。

(2)线程上下文

双亲委派模式自身存在缺陷,即上级加载器无法使用下级加载器的类。而实际引用中存在上级定义规范,下级实现的情况,如JDBC。JDBC是Java定义的规范接口,需要实际的数据库厂商根据自身特点来实现,而JDBC接口是Java核心类库定义的,应该属于引导类加载器管理;而数据库厂商实现的自然是应用程序类加载器,此时如果依旧采用纯双亲委派机制就无法实现JDBC的需求。

为了解决这个困境,Java设计团队引入了线程上下文类加载器。线程上下文类加载器运行父类加载器去请求子类加载器完成类的加载的行为。这便破坏了双亲委派机制。

图:线程上下文加载器

tAQjU4vlirpD8wk

(3)热代码替换

这一次破坏源于用户对程序动态性的追求,Hot Swap,Hot Deployment等。

IBM主导的JSR-291实现的模块热部署采用了自定义类加载器,该自定义加载器不再使用双亲委派模型推荐的树形结构,而是采用的复杂的网状结构。

注意

“被破坏”不意味着就是贬义,只要理由充分,突破旧的原则无疑就是一种创新。

三、总结

JVM通过ClassLoader实现类的加载。ClassLoader在加载时会遵循双亲委派机制,即在实现类加载时递归请求委派父类加载器区加载,直到顶层父类无法加载,才会返回让当前类机制器实现加载。这种方式既可以避免类的重复加载,确保类的全局唯一,又可以保证程序安全,防止核心API被褚篡改。

同时双亲委派模型不是一个强制性模型,JDK9 前,历史上出现了3次破坏双亲委派模型的情况:

  • JDK1.2前:双亲委派机制是JDK1.2才引入,之前自定义的不具有双亲委派机制。
  • 线程上下文:双亲委派机制本身具有缺陷,即上级加载器无法使用下级加载器的类,而实际中又存在这种场景,如:JDBC。
  • 热代码替换【了解】。

四、参考

评论