类加载过程和双亲委派模型

类加载过程和双亲委派模型

简述类加载过程

详细流程,看一下 《深入理解 Java 虚拟机》中的虚拟机的类加载章节

一般来说我们把 Java 的类加载过程分为三个主要步骤(其中链接又细分了三个子步骤):

  • 加载 (Loading)
    • 将 Java 字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象)。如果输入的数据不满足 ClassFile 结构(Java 虚拟机规范中的4.1和4.8章节内容),则会抛出 ClassFormatError
      1. 这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至可以是网络数据源等;
      2. loading 阶段是用户可以参与的阶段,除了 JVM 内置的三种类加载器外,我们可以自定义类加载器,实现自己的类加载过程;
  • 链接 (Linking)(有三个子步骤)

    这是核心的步骤,简单说就是把原始的类定义信息平滑地转入 JVM 运行过程中。

    • 验证 (Verification)
      • 验证字节信息是符合 Java 虚拟机规范的(4.9章节内容),否则就抛出 VerifyError,这样可以防止恶意信息或者不合规的信息危害 JVM 的运行。这是 JVM 安全的重要保障。

        verification 阶段可能触发更多 class 的加载,但不需要对它们进行验证和准备。

        问题:额外触发的类加载,它们的 linking 什么时候来做?排队等当前linking 做完?

    • 准备 (Preparation)
      • 创建类或接口中的静态变量,并为它们赋初始值(分配内存空间)。

        但是这里的赋初始值,与后面显示初始化阶段(initialization)的内容有区别,侧重点在于分配变量所需要的内存空间,并不会执行更进一步的 JVM 指令。

    • 解析 (Resolution)
      • 将运行时常量池中的符号引用(symbolic reference)替换为直接引用。
      • Java 虚拟机指令 anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokvirtual, ldc, ldc_w, multianewarray, new, putfield,和putstatic 在运行时常量池中是符号引用。这些指令的执行都需要解析其符号引用变为直接引用。

        上面提到的指令,除 invokedynamic 之外,其他指令解析一次其符号引用即可。

        invokedynamic 指令解析的具体值是绑定到具体的 invokedynamic 调用位置的。

      • 解析是动态地从运行时常量池中的符号引用确定具体值的过程。

        符号引用可以理解为是标记的标签,设置一个接话,暂时没有人选,设定该人员为 A ,等开始做的时候,确定让小明去做。解析就是把 A(符号引用)替换为小明(直接引用)。符号引用就是一个字面量,没有什么实质性的意义,只是一个代表。直接引用指的是一个真实引用,在内存中可以通过这个引用查到目标。

  • 初始化 (Initialization)
    • 这一步骤是真正去执行类初始化的代码逻辑,包括静态字段赋值,执行静态代码块的逻辑,父类型的初始化逻辑优先当前类的逻辑。
    • 下面5个操作会对类或接口 C 进行初始化操作:
      1. 执行任何引用C 的Java 虚拟机指令 new, getstatic, putstatic或invokestatic (new, getstatic, putstatic, invokestatic) 。这些指令通过字段引用或者方法引用直接或间接引用类或接口C,则会初始化C。
      2. 在类库中调用某些反射方法,例如,在类class或者java.lang.reflect中调用。
      3. 如果C 是一个类,它的一个子类初始化,则类C 也会被初始化。
      4. 如果C 是一个声明非抽象、非静态方法的接口,则直接或间接实现C 的类初始化,也会初始化C。
      5. 如果C 是一个类,它在Java 虚拟机启动时被指定为初始类,则会初始化C。

补充:

  1. JVM 是动态加载,链接和初始化类和接口的;
  2. 在 JVM 层面,每个 Java 类的构造函数都是实例初始化的方法。编译器将该方法命名为 init

类加载器

通过查看 Java 虚拟机规范 内容我们可以知道,对于 Java 虚拟机来说,只有两种类加载器:

  • 启动类加载器(bootstrap class loader)
    • 这个类加载器使用 C/C++ 语言实现,是虚拟机自身的一部分;(Hotspot 使用 C++,JRockit 和 J9 使用 C,具体可以参考《深入理解Java虚拟机》虚拟机类加载机制,章节)
      • 虚拟机自身一部分,这样理解,虚拟机自身是由 C/C++ 实现的,启动类加载器是在虚拟机内部实现的;其他的核心类库都是属于虚拟机外部
  • 其他类加载器(虚拟机规范中称为:user-defined class loader)
    • 这些类加载器都是有 Java 语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader
      • 包括扩展类加载器和应用类加载器都在 rt.jar 核心库中,由 Java 语言实现,虚拟机启动时加载创建,都属于 user-defind class loader
      • JDK提供的内置类加载器的关系查看下面内容。

类与类加载器

Java 虚拟机规范

We will sometimes represent a class or interface using the notation <N, Ld>, where N denotes the name of the class or interface and Ld denotes the defining loader of the class or interface.

We will also represent a class or interface using the notation NLi, where N denotes the name of the class or interface and Li denotes an initiating loader of the class or interface.

从上面内容我们可以知道,

对于任何一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性,每个类加载器,都拥有一个独立的类名称空间。

更通俗一些:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个 Class 文件,被同一个 Java 虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

  • 这里所指的“相等”,包括代表类的 Class 对象的 equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用 isstanceof 关键字做对象所属关系判定等各种情况。
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
39
40
41
42
package com.spoonli.mall.jvm;

import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {

public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
// 用自定义的类加载器加载该类并实例化
Object obj = myLoader.loadClass("com.spoonli.mall.jvm.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
// 判断对象是否为 ClassLoaderTest 类型
System.out.println(obj instanceof ClassLoaderTest);
// 查看通过自定义类加载器加载并实例化的对象的类加载器与 jvm内加载的ClassLoaderTest的类加载器;
System.out.println(obj.getClass().getClassLoader());
System.out.println(ClassLoaderTest.class.getClassLoader());
}
}

------
运行结果:
class com.spoonli.mall.jvm.ClassLoaderTest
false
com.spoonli.mall.jvm.ClassLoaderTest$1@7e32c033
sun.misc.Launcher$AppClassLoader@18b4aac2

上面代码构造了一个简单的类加载器,从运行结果看,尽管来自同一个 Class 文件,但是自定义类加载器加载的创建的对象,在做所属类型判断时返回了 false 。因为此时 Java 虚拟机中同时存在两个 ClassLoaderTest 类,一个由 jvm 虚拟机的应用程序类加载器加载的,一个是我们自定义的类加载器加载的。在虚拟机中是两个相互独立的类,所以所对象所属类型检查时的结果为 false 。

这段来自 《深入Java虚拟机》第3版P281。

Java 8 及其之前,JDK 提供的三种内置类加载器

  • 启动类加载器(Bootstrap Class Loader)

    • 它属于虚拟机自身的一部分,用 C++ 实现的,主要负责加载 jre/lib 目录中的 jar 文件,如 rt.jar,或被 -Xbootclasspath 参数指定的路径中的并且文件名是被虚拟机识别的文件
  • 扩展类加载器(Extension or Ext Class Loader)

    • 它是 Java 实现的,独立于虚拟机,负责加载jre/lib/ext目录下的 jar 包,这就是所谓的 extension 机制,该目录也可以通过设置 java.ext.dir 系统参数来覆盖
      1
      java -Djava.ext.dirs=xxx your_app
  • 应用类加载器(Application or App Class Loader)

    • 它是 Java 实现的,独立于虚拟机。负责加载用户指定的 classpath的内容。

      有个容易混淆的概念,系统(system)类加载器,通常来说,其默认就是 JDK 内建的应用类加载器,但是它同样可以通过系统参数修改,例如

    1
    java -Djava.system.class.loader=com.yourapp.YourClassLoader your_app

    注意,如果我们指定了system参数,JDK 内建的应用类加载器就会成为我们自定义加载器的父亲,这种方式通常在类似需要改变双亲委派模型的场景。

所以,一般情况(没有自定义类加载器时),类加载会从应用类加载器委托给扩展类加载器,再委托给启动类加载器,启动类加载器找不到然后扩展类加载器找,扩展类加载器找不到再由应用类加载器找。

双亲委派模型

如何看出三种内置加载器的父子关系?

  1. AppClassLoaderExtClassLoader

AppClassLoader,ExtClassLoader是由sun.misc.Launcher初始化的,查看LauncherClassLoader 源码的构造方法可以知道AppClassLoaderExtClassLoader的父子关系由Launcher保证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 下面源码摘录自 sun.misc.Launcher.class(jdk1.8.0_291)
// 代码为IDE反编译获得,所以变量名可读性较弱,但不影响理解
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// var1 是 ExtClassLoader 变量,这里获取 ExtClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}

try {
// ExtClassLoader 即 var1 作为入参,传入了 getAppClassLoader(var1)
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}

Thread.currentThread().setContextClassLoader(this.loader);
//省略其他代码
...
}

沿着 getAppClassLoader() 方法,根据对应参数和涉及方法,最后可以追踪到 ClassLoader 的构造方法中,可以看到 getAppClassLoader(var1) 中传入的参入 var1 最终被保存在 ClassLoaderparent 成员变量中。

1
2
3
4
5
6
private ClassLoader(Void unused, ClassLoader parent) {
// 在 ClassLoader 中 parent 就是当前类加载器的父加载器,在 loadClass() 方法中,即父委派模型的具体实现代码中,可以看到 parent 的作用。
this.parent = parent;
// 省略部分代码
...
}

综上所述,AppClassLoaderExtClassLoader 的父子关系由 Launcher 保证。

  1. BootStrapClassLoaderExtClassLoader 父子关系如何确定的?

ClassLoaderloadClass() 的源码逻辑提供了答案。

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
// 下面源码摘录自 java.lang.ClassLoader.java (jdk1.8.0_291)
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) {
// 如果父加载器抛出 ClassNotFoundException 异常,
// 说明父加载器无法完成加载请求。
}

if (c == null) {
// 在父加载器无法加载时,再调用本身的 findClass 方法进行加载。
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

我们注意到,在加载类的过程中,找不到 parent 的时候,会首先调用 findBootstrapClassOrNull(name) 去尝试返回由 BootStrapClassLoader 加载的 Java 核心类。这种机制便保证了 BootStrapClassLoader 是所有ClassLoader 的父加载器。

bootstrap class loader 作为顶级类加载器,没有 parent ,所以 parent == null,就可以表示使用的是 bootstrap class loader。

Java 9开始,Java 类加载器的变化

在 JDK 9 中,由于 Jigsaw 项目引入了 Java平台模块化系统(JPMS),Java SE 的源代码被划分为一系列模块。

类加载器,类文件容器等都发生了非常大的变化。

  • 前面提到的 -Xbootclasspath即修改启动类加载器加载目录的参数不可用了。API 已经被划分到具体的模块中了,所以编程了对相应模块进行修改的方式了。

    1
    2
    # 假设修改的模块为 java.base,那么先编译好相关模块,并替换 java.base 模块,然后通过下面参数替换。
    java --patch-module java.base=your_path yourApp
  • 扩展类加载器被重命名为平台类加载器(Platform Class Loader),而且 extension 机制被移除。也就意味着,如果我们指定 java.ext.dirs系统变量,或者 lib/ext 目录存在,则 JVM 将直接返回错误!

    建议解决办法就是将其放入 classpath 中。

  • 部分不需要 AllPermission 的 Java 基础模块,被降级到平台类加载器中,相应的权限也被更精细粒度地限制起来。

  • rt.jar 和 tools.jar 同样被移除了,JDK 的核心类库以及相关资源,被存储在 Jimage 文件中,并通过新的 JRT 文件系统访问,而不是原有的 JAR 文件系统。但是对大部分软件兼容性影响不大。

  • 增加了 Layer 的抽象,JVM 启动默认创建 BootLayer,开发者也可以自定义和实例化 Layer,可以更加方便的实现类似容器一般的抽象逻辑。

  • 内建类加载器器都在 BootLayer中,其他 Layer内部有自定义的类加载器,不同版本模块,可以同时工作在不同的 Layer

通常类加载机制的基本特征

  1. 双亲委派模型。但不是一种强制性约束,它是一种 JAVA 设计者推荐使用类加载器的方式。所以不是所有类加载都遵守这个模型,比如 JDK 内部的 SPI 机制。用户可以在标准 API 框架上,提供自己的实现。这种机制不会用双亲委派模型去加载,而是利用所谓的上下文加载器

    关于 SPI 机制,和上下文加载器,参考TODO

  2. 类可见性,一个类加载器只能看到由他自己家或是其父辈类加载器加载的类,它自己是看不到更低层级类加载器所加载的类的。

    • 例如,如果父加载器(ExtClassLoader)需要加载的类$JAVA_HOME/jre/ext/xxx.jar#Class A 引用了存在于更低层级类加载器 AppClassLoader负责范围($class_path)中才存在的类,那么在加载过程中就报错。
    • 当这种需求出现的时候,可以使用 JDK 提供的另一种类加载器 ContentClassLoader 予以解决。
  3. 单一性,由于父加载器的类对自家在其是可见的,所以父加载器加载过的类,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一个类仍然可以被加载多次,因为互相并不可见。

  4. 全盘负责原则,当一个 classloader 加载一个 Class 的时候,这个 Class 所依赖的和引用的其他 Class 通常 也由这个 classloader 负责加载;

  5. cache 机制,如果 cache 中保存了这个 class 就直接返回它,如果没有才从文件中读取和转换成 Class,并存入 cache。(这就是为什么修改了 class 但是必须重启 JVM 才能生效,并且类只加载一次的原因。)

自定义类加载器

常见的场景:

  • 实现类似进程内隔离,类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果。这方便的集大成者是 Java EE,OSGI,JPMS等框架
    • 例如,两个模块依赖某个类库的不同版本,如果分别被不同的容器加载,就可以互补干扰。
  • 应用需要从不同的数据源获取类定义信息。
    • 例如,网络数据源;
  • 需要自己操纵字节码,动态修改或生成类。

简单理解自定义类加载过程:

  1. 指定类或接口的名称,找到其二进制描述并加载,(这里往往就是自定义类加载器会“定制”的部分),例如在特定数据源根据名字获取字节码,或者修改或生成字节码。
  2. 创建 Class 对象,并完成类加载过程。二进制信息到 Class 对象的转换,通常就是依赖 defineClass,我们无需自己实现,它是 final 方法。有了 Class 对象,后序完成加载过程就顺利成章了。

具体实现可以参考用例

JDK 目前对 “java.”开头的包增加了权限保护,在自定义类加载器的时候,可以将这些包仍然交给 jdk 加载。

1
2
3
if (name.startWith("java.")) {
return ClassLoader.getSystemClassLoader().loadClass(name);
}

即使再重写 loadClass(String name)方法时,也只需要重写我们需要的部分,可以将父类中 loadClass()方法的相关内容抄过来。

1
2
3
4
5
6
7
8
9
10
11
12
例如:
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c == null) {
if (name.startWith("java.")) {
return ClassLoader.getSystemClassLoader().loadClass(name);
}
// 执行我们重写的 findClass(name) 方法;
return findClass(name);
}
}

什么是双亲委派模型?

简单说,就是当类加载器(Class Loader)试图加载某个类的时候,首先将这个类加载请求委派给父加载器去完成,如果父加载器还有父类则接着向上委托,一直递归到顶层。当父加载器找不到相应的类,无法完成这个请求时,子类才会尝试去加载。

使用委派模型的目的,是避免重复加载 Java 类。

注:这里的“双亲”是翻译的有些问题的,称为 “父委派模型” 可能更合适,因为每一层只有一个父加载器,并不能称为双亲(父母两个),不过既然流传已久,知道这里的“双亲委派模型”中的双亲的真实含义即可。可以理解为一种长久的翻译错误。

委派模型的 Oracle 官方出处: The Java Class Loading Mechanism

The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a "parent" class loader. When loading a class, a class loader first "delegates" the search for the class to its parent class loader before attempting to find the class itself.

翻译一下就是:

Java平台使用委托模型来加载类。基本思想是,每个类加载器都有一个“父”类加载器。当加载类时,类加载器首先将查找类的任务“委托”给它的父类加载器,然后再尝试自己去加载这个类。

文档中也介绍了具体实现,查看源码 java.lang.ClassLoaderloadClass方法。

但是父类委派模型的基本特性没有找到出处:

  • 单一性

  • 一般性

  • 可见性

  • 全盘负责

上面这些网上找来的委派模型和类加载器的原则,暂时没有找到官方出处。

如何定义符合父委派模型的类加载器?

  1. 首先,自定义一个类加载器继承ClassLoader;
  2. 重写 ClassLoader 中的findClass(String name) 方法,
    • 在方法中自行实现读取 class 文件为 byte 数组;
    • 调用 defineClass 方法将 byte 数组解析加载为类;

经过之前分析 loadClass()方法,如果父辈加载失败,会自动调用自己的 findClass()方法来完成加载。

如何自定义一个违背父委派模型的类加载器?

  1. 首先,自定义一个类加载器并继承ClassLoader;
  2. 重写 ClassLoader中的loadClass(String name)方法;

总结:自定义类加载器时,重写 findClass(String name)方法会遵循父委派模型,重写loadClass(String name) 方法会破坏/违背父委派模型。

有哪些没有按照双亲委派模型实现的例子?

即,打破/违反了双亲委派模型

上面提到的 [通常类加载机制的三个基本特征](# 通常类加载机制的三个基本特征) 中有写,SPI 机制没有遵守双亲委派模型。例如 Java 中 JNDI,JDBC等都是利用这种机制。

为什么说 JDBC 破坏了/没有遵守双亲委派模型?

重点在 DriverManager的静态代码块:

1
2
3
4
5
6
7
8
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

在由 Bootstrap Class Loader 加载的类中,加载了由 App Class Loader 加载的类,破坏了类加载器机制中的可见性,所以没有遵守双亲委派模型?直接使用了上下文加载器就避开破坏可见性的问题了。?是这样吗?

DriverManagerclassloader 是bootstrap,而得到的 connectionclassloader是application。如果在A类引用B类时发现B类还没有加载,那么会调用A类的类加载器进行加载,并且由于可见性的原因,bootstrap加载的类是看不到ext或者application加载的类的,对应这里的DriverManagerconnection,所以说SPI破坏了双亲委任模型。

可见性?在哪里规定的?

这是创建 SPI 的原因吗?

没有SPI时,你可以现在classpath里加一个mysql-connector-java.jar,然后这样写

1
2
Class clz = Class.forName("com.mysql.jdbc.Driver");
Driver d = (Driver) clz.newInstance();

这就没问题了,这里用了Application Classloader加载了mysql-connector-java.jar的com.mysql.jdbc.Driver。问题是你这里进行了硬编码(hard code),即一定要加载”com.mysql.jdbc.Driver”,不是很优雅,不能实现“用接口编程,自动实例化真的实现“的这种编码形式。

问题:

  1. 为什么 Bootstrap Class Loader 加载器只负责加载 jre/lib 目录下的文件?是在哪里硬编码(hardcode )了吗?
  2. 从哪里得出三个内置加载器的关系?
  3. Class.forName() vs classloader.loadClass()?

Tomcat 如何违背父委派模型,以及为什么违背

首先,tomcat 官方文档中描述了其自定义的类加载器层级关系:

When Tomcat is started, it creates a set of class loaders that are organized into the following parent-child relationships, where the parent class loader is above the child class loader:

1
2
3
4
5
6
7
    Bootstrap ($JAVA_HOME/jre/lib/ ; $JAVA_HOME/jre/lib/ext)
|
System ($CATALINA_HOME/bin/bootstrap.jar ; $CATALINA_BASE/bin/tomcat-juli.jar / $CATALINA_HOME/bin/tomcat-juli.jar ; $CATALINA_HOME/bin/commons-daemon.jar)
|
Common ($CATALINA_BASE/lib ; $CATALINA_HOME/lib)
/ \
Webapp1 Webapp2 ... (/WEB-INF/classes ; /WEB-INF/lib )

如何违背父委派模型

每一个 webapp classloader 在加载类时,会优先在 WEB-INF/classesWEB-INF/lib 中搜索并尝试加载,而不是优先委托给父加载器尝试加载。

这样做的好处是它允许不同的 web 项目去重载 Tomcat 提供的 lib 包(如 $CATALINA_HOME/lib/中的 jar 包)。

这极大程度上保证了不同 web 项目的独立性和自由度。

为什么违背

Tomcat 作为一个服务器容器,需要有同时运行多个 war 包的能力,而每个 war 包中都拥有自己的依赖 lib 库(WEB-INF/lib)以及各自的项目代码(WEB-INF/classes),为了保证每个 web 项目可以共同运行,互不干扰,Tomcat 为每个项目都创建一个单独的 webapp classloader,它会负责加载对应的 web 项目下 WEB-INF/classes 的 class 文件和资源以及 WEB-INF/lib下的 jar 包中所包含的 class 文件和资源文件,使得这些被加载的内容仅对该 web 项目可见,对其他 web 项目不可见。

类加载器的可见性,自带隔离特性。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信