代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。
上两篇文章我们分别讨论了类加载的时机和类加载的过程,本篇来讨论类加载器和双亲委派模型。虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为“类加载器”。
类与类加载器
- 对于任意的一个类,都需要
由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中唯一性
,每一个类加载器,都拥有一个独立的类名称空间 (这也是为什么每个类的初始化只会执行一次的原因)。通俗的来讲:比较两个类是否“相等”,只有这两个类由同一个类加载器加载的前提
下才有意义。否则,即使这两个类来源于同一个 .class 文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必然不相等。
双亲委派模型
- 从虚拟机的角度来说,只存在两种不同的类加载器:
一种是启动类加载器
(Bootstrap ClassLoader),该类加载器使用 C++ 语言实现,属于虚拟机自身的一部分。另外一种就是所有其它的类加载器
,这些类加载器是由 Java 语言实现,独立于 JVM 外部,并且全部继承自抽象类 java.lang.ClassLoader
。 - 从 Java 开发人员的角度来看,大部分 Java 程序一般会使用到以下三种系统提供的类加载器:
- 启动类加载器(Bootstrap ClassLoader):负责加载
<JAVA_HOME>\lib
目录中并且能被虚拟机识别的类库到 Java 虚拟机内存中。如果名称不符合的类库即使放在 lib 目录中也不会被加载。启动类加载器无法被 Java 程序直接引用
,用户在编写自定义加载器时,如果需要把加载请求委派给启动类加载器,那么直接用 null 代替即可。 - 扩展类加载器(Extension ClassLoader):这个加载器由
sun.misc.Launcher.ExtClassLoader
实现,他负责加载<JAVA_HOME>\lib\ext
目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器
。 - 应用程序类加载器(Application ClassLoader):这个类加载器由
sun.misc.Launcher.AppClassLoader
实现。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(Classpath)
上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
。
- 启动类加载器(Bootstrap ClassLoader):负责加载
- 我们的应用程序都是由这三类加载器互相配合进行加载的,我们也可以加入自己定义的类加载器。这些类加载器之间的关系如下图所示(图片来源网络):
- 如上图所示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parent Delegation Model)。该模型要求
除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器
。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合
(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是
把这个请求委派给父类加载器去完成
,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器
,只有当父加载器反馈自己无法完成该加载请求
(该加载器的搜索范围中没有找到对应的类
)时,子加载器才会尝试自己去加载。 - 双亲委派模型对于保证 Java 程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在 java.lang.ClassLoader 的 loadClass() 方法之中,如下代码所示,逻辑清晰易懂:
先检查是否已经被加载过,若没有加载则调用父类加载器的 loadClass() 方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器失败,将会抛出 ClassNotFoundException 给子类,子类再调用自己的 findClass() 方法进行加载,以此循环往复,直到有类加载器加载该类。
public abstract class ClassLoader {
...
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
...
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
...
c = findClass(name);
// do some stats
...
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
...
}
破坏双亲委派模型
书上说:
- 双亲委派模型的第一次“被破坏”其实
发生在双亲委派模型出现之前
,即 JDK1.2 发布之前。由于双亲委派模型是在 JDK1.2 之后才被引入的,而类加载器和抽象类 java.lang.ClassLoader 则是 JDK1.0 时候就已经存在,面对已经存在 的用户自定义类加载器的实现代码,Java 设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容
,JDK1.2 之后的 java.lang.ClassLoader 添加了一个新的 proceted 方法 findClass(),在此之前,用户去继承 java.lang.ClassLoader 的唯一目的就是重写 loadClass() 方法,因为虚拟在进行类加载的时候会调用加载器的私有方法 loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的 loadClass()。JDK1.2 之后已不再提倡用户再去覆盖 loadClass() 方法,应当把自己的类加载逻辑写到 findClass() 方法中,在 loadClass() 方法的逻辑里,如果父类加载器加载失败,则会调用自己的 findClass() 方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型的。 - 双亲委派模型的第二次“被破坏”是
这个模型自身的缺陷所导致的
,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的 API。但是,如果基础类又要调用用户的代码,那该怎么办呢。这并非是不可能的事情,一个典型的例子便是 JNDI 服务,它的代码由启动类加载器去加载(在 JDK1.3 时放进 rt.jar),但 JNDI 的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的 classpath 下的 JNDI 接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?为了解决这个困境,Java 设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器
(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader() 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。有了线程上下文类加载器,JNDI 服务使用这个线程上下文类加载器去加载所需要的 SPI 代码,也就是父类加载器请求子类加载器去完成类加载动作
,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型
,但这也是无可奈何的事情。Java 中所有涉及 SPI 的加载动作基本上都采用这种方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。 - 双亲委派模型的第三次“被破坏”是由于
用户对程序的动态性的追求导致的
,例如 OSGi 的出现。在 OSGi 环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。 - 虽然说这里使用了“被破坏”这个词来形容上述不符合双亲委派模型原则的行为,但这里“被破坏”并不带有贬义的感情色彩。
只要有足够的意义和理由,突破已有的原则就可认为是一种创新。
以上有些晦涩,这里有场景:
- JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。
- Tomcat 应用的类加载器
优先自行加载应用目录下的 class
,并不是先委派给父加载器,加载不了才委派给父加载器。tomcat 之所以造了一堆自己的 classloader,大致是出于下面三类目的:- 对于各个 webapp 中的 class 和 lib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的 lib 以便不浪费资源;
- 与 jvm 一样的安全性问题。使用单独的 classloader 去装载 tomcat 自身的类库,以免其他恶意或无意的破坏;
- 热部署。
- OSGi 实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换,其类加载的过程中,有平级的类加载器加载行为;打破的原因是为了实现模块热替换。
- JDK 9,ExtensionClassLoader 被 PlatformClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载;打破的原因,是为了添加模块化的特性。
笔记来源:《深入理解Java虚拟机》第七章 虚拟机类加载机制 7.4 类加载器(P227)。