JAVA 虚拟机中的动态类加载

原文连接:https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.18.762&rep=rep1&type=pdf

0 摘要

ClassLoader 是在 Java 平台上动态装入软件组件的强大机制,它们在以下这些特性的支持上非常有意思:

  • laziness
  • type-safe linkage
  • user-defined extensibility
  • multiple communicating namespaces

本篇文章将介绍 ClassLoader 的概念,并演示它们的一些特别用途。此外,本文还讨论了如何在用户定义的动态类加载中维护类型安全

1 介绍

在本文中,我们研究了 Java 虚拟机的一个重要特性:动态类加载。这就是提供 Java 平台强大功能的底层机制-在运行时安装软件组件的能力。一个典型的例子是动态下载到 web 浏览器中的 applet。虽然许多其他系统也支持某种形式的动态加载和链接,但是 Java 平台是这些系统中唯一包含以下所有特性的平台:

  • Lazy loading:类是按需加载的,类加载应该被尽可能地延迟,从而减少内存的使用并改善系统响应时间。
  • Type-safe linkage:动态类加载不能违反 Java 虚拟机的类型安全。为了保证类型安全性,动态加载必须不需要额外的运行时检查。附加的链接时间检查是可以接受的,因为这些检查只执行一次。
  • User-definable class loading policy:ClassLoader 对象也是 Java 中的一类对象,所以开发者完全可以控制动态类加载的行为。比如,用户定义的 ClassLoader 可以指定被加载类的远端位置,或者为从特定源加载的类分配适当的安全属性。
  • Multiple namespaces:ClassLoader 为不同的组件提供单独的 namespaces。例如,Hotjava浏览器从不同的源将 applet 加载到单独的类加载器中。这些 applet 可能包含相同名称的类,但是 Java 虚拟机将这些类视为不同的类型(classloader 不同)。

相比之下,现有的动态链接机制并不支持所有这些特性。尽管大多数操作系统都支持某种形式的动态链接库,但这样的机制是针对C/ c++代码的,并且不是类型安全的。Lisp、Smalltalk 和 Self 等动态语言通过附加的运行时检查(而不是链接时检查)来实现类型安全。

本文的主要研究是首次对 ClassLoader 进行深入的描述,ClassLoader 是 Java 平台引入的一个概念,ClassLoader 从 JDK 1.0 版本就已经提供了,其最开始的目前是用于在 Hotjava 浏览器中动态加载 applet 类。也就是从那时起,ClassLoader 的使用得到了扩展,以处理范围更广的组件和场景。如服务器端组件(servlet)、Java平台的扩展机制、以及 javabean 组件。尽管 ClassLoader 的作用越来越重要,但相关文章中并没有对其底层机制进行充分的描述。

本文的另一个研究点是通过 ClassLoader 为长期存在的类型安全问题提供了一种解决方案。JDK 的早期版本(1.0和1.1)在 ClassLoader 实现中包含一个严重的缺陷。编写不当的 ClassLoader 可能破坏 Java 虚拟机的类型安全保证。请注意,类型安全问题没有直接带来任何安全风险,因为不受信任的代码(例如下载的applet)不允许创建 ClassLoader。尽管如此,需要编写自定义 ClassLoader 的应用程序开发者可能会无意中损害类型安全。

虽然这个问题已经存在一段时间了,但是这个问题在业界还没有非常普适的解决方案。例如,前面的讨论集中在类型安全性的缺乏是否是用户可定义 ClassLoader 的基本限制,以及我们是否必须限制 ClassLoader 的能力、放弃延迟类加载或在运行时引入额外的动态类型检查。我们在本文中提出的解决方案已经在 JDK 1.2 中实现,解决了类型安全问题,同时保留了 ClassLoader 的所有其他需要的特性。

我们假设读者具有 Java 编程语言的基本知识。本文的其余部分组织如下:首先对 ClassLoader 进行更详细的介绍。第 3 节讨论 ClassLoader 的应用程序。第 4 节描述由于使用 ClassLoader 而可能出现的类型安全问题及其解决方案。最后给出结论。

2 关于 ClassLoader

ClassLoader 的主要作用就是支持在 java 平台上动态加载软件组件。软件分发的基本单元是 class(类)。 classes 以一种平台无关的、独立的、标准的二进制类文件格式形式分发。单个类的表示称为类文件。类文件由 Java 编译器生成,可以加载到任何 Java 虚拟机中。类文件不需要存储在实际文件中;它可以存储在内存缓冲区中,或者从网络流中获得。

Java虚拟机执行存储在类文件中的字节代码。然而,字节码序列只是虚拟机执行程序所需要的一部分。类文件还包含对字段、方法和其他类名称的符号引用。例如,class C 的声明如下:

1
2
3
4
5
6
class C {
void f(){
D d = new D();
// ...
}
}

表示 C 的类文件包含对类 d 的符号引用。符号引用在链接时解析为实际的类类型。类类型是 Java 虚拟机中具体化的 first-class 对象。类类型在用户代码中表示为类 java.lang.Class 的对象。为了解析对类的符号引用,Java虚拟机必须加载类文件并创建类类型。

first-class: 简而言之,这意味着对对象的使用没有任何限制。它和其它对象一样。first-class 对象是可以动态创建、销毁、传递给函数、作为值返回的实体,并且具有编程语言中其他变量具有的所有权限。
https://stackoverflow.com/questions/245192/what-are-first-class-objects

类类型在用户代码中表示为类java.lang.Class的对象。为了解析对类的符号引用,Java虚拟机必须加载类文件并创建类类型。

类加载概述

Java 虚拟机使用 classloaders 去加载类文件和创建 class 对象。classloader 也是普通的对象,可以使用 java 代码编写定义,但是它们必须是 ClassLoader 这个抽象类的子类,ClassLoader 代码如下所示(省略其他无关代码):

1
2
3
4
5
6
7
class ClassLoader{
public Class loadClass(String name);
protected final Class defineClass(String name, byte[] buf, int off, int len);
protected final Class findLoadedClass(String name);
protected final Class findSystemClass(String name);
// ...
}

在上图的描述中, ClassLoader.loadClass 方法接受一个类名作为参数,并返回一个类对象,该类对象是类类型的运行时表示。关于 defineClass、findLoadedClass、findSystemClass 下面再聊。
在上面的案例中,假设类 C 是被 classloader L 加载的,那么 L 就是 C 的 defining loader,java 虚拟机将使用 L 去加载 C 引用的类。在虚拟机分配类 D 的对象之前,它必须解析对 D 的引用,如果还没有加载 D,虚拟机将调用 C 的类加载器 L 的 loadClass 方法来加载 D:

1
L.loadClass("D")

加载 D之后,虚拟机就可以解析了引用并创建类 D 的对象。

多 Class Loaders

Java 应用程序可以使用几种不同类型的类装入器来管理各种软件组件。下图显示了用 Java 编写的 web 浏览器如何使用类加载器。
image.png

这个示例演示了两种类型的 ClassLoader 的使用:用户定义的 ClassLoader 和 Java 虚拟机提供的系统 ClassLoader。用户定义的 ClassLoader 可用于创建来自用户定义的源的类。例如,浏览器应用程序为下载的 applet 创建类加载器。我们为 web 浏览器应用程序本身使用一个单独的类加载器,所有系统类(如java.lang.String)都被加载到系统类加载器中,Java 虚拟机直接支持系统类装入器。

图中的箭头表示 ClassLoader 之间的委托关系;ClassLoader L1 可以委托另一个ClassLoader L2 代表自身去加载类 C,在这种情况下,L1 委托 C 给 L2。例如,applet 和 应用程序类 ClassLoader 将所有系统类委托给系统类 ClassLoader,因此,所有系统类在 applet 和应用程序之间共享。这是可以的,因为如果 applet 和系统代码对类型 java.lang.String 有不同的概念,就会违反类型安全性。委托 ClassLoader 加载允许我们在共享一组公共类的同时保持名称空间隔离,在 Java 虚拟机中,类类型唯一由类名和 ClassLoader 的组合决定,Applet 和应用程序 ClassLoader 委托给系统类装入器,这就保证了所有的系统类型(java.lang.String)的唯一性。另外,在 applet 1 中加载的名为 C 的类被认为是不同于 applet 2 中名为 C 的类的类型,尽管这两个类具有相同的名称,但它们是由不同的 ClassLoader 定义的。事实上,这两个类是完全不相关的。例如,它们可能有不同的方法或字段。

一个 applet 中的类不能干扰另一个 applet 中的类,因为 applet 是有各自单独的 ClassLoader 中加载的,这对于保证 Java 平台安全性至关重要。同样,由于浏览器位于单独的 ClassLoader 中,因此 applet 不能访问用于实现浏览器的类,applet 只允许访问在系统类中公开的标准 Java API。

Java 虚拟机通过创建应用程序 ClassLoader 并使用它加载初始的浏览器类来启动,应用程序在初始类的公共类方法 void main(String[]) 中开始执行,该方法的调用驱动所有的进一步执行,指令的执行可能导致附加类别的加载,在这个应用程序中,浏览器还为下载的 applet 创建额外的ClassLoader。

垃圾收集器卸载不再引用的 applet 类,每个类对象都包含一个对其定义 ClassLoader 的引用;每个 ClassLoader 引用它定义的所有类,从垃圾收集器的角度来看,这意味着类与它们的定义 ClassLoader 是强连接的,当类的定义 ClassLoader 垃圾收集时,类将被卸载。

ClassLoader 的例子

下面介绍一个简单的 ClassLoader 的实现。如前所述,所有用户定义的 ClassLoader 类都是 ClassLoader 的子类。ClassLoader 的子类可以覆盖loadClass 方法,从而提供用户定义的加载策略。这是一个在给定目录中查找类的自定义 ClassLoader :

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
/**
* @author: guolei.sgl (guolei.sgl@antfin.com) 2020/12/4 7:42 下午
* @since:
**/
public class FileClassLoader extends ClassLoader {

private String directory;

public FileClassLoader(String directory){
this.directory = directory;
}

public synchronized Class loaderClass(String name) throws ClassNotFoundException{
Class c = findLoadedClass(name);
if (c != null) {
return c;
}

try {
c = findSystemClass(name);
} catch (ClassNotFoundException e){
// keep looking
}


try {
byte[] data = getClassData(directory,name);
return defineClass(name,data,0,data.length);
} catch (IOException e){
// keep looking
throw new ClassNotFoundException();
}
}

private byte[] getClassData(String directory, String name) throws IOException{
// 省略
return null;
}
}

公共构造函数 FileClassLoader() 只记录目录名。在 loadClass 的 定义中,我们使用 findLoadedClass 方法检查类是否已经加载。如果findLoadedClass 返回 null,则表示该类尚未加载。然后,我们通过调用 findSystemClass 委托给系统类加载器,如果我们试图加载的类不是系统类,我们就调用 getClassData方法来读取类文件。

在读入类文件之后,我们将其传递给 defineClass 方法,defineClass 方法从类文件 构造类的运行时表示(即 Class 对象)。请注意,loadClass 方法使用了 synchronized 关键字,这样也保证了多个线程加载同一个类时的线程安全性。

类的初始化和 Defining Loaders

当一个 classloader 委托给另一个 classloader 时,发起 load 的 classloader 不一定是完成 class 加载并定义的 classloader,如下代码片段:

1
2
3
4
5
6
try {
FileClassLoader cl = new FileClassLoader("/foo/bar");
Class stringClass = cl.loaderClass("java.lang.String");
} catch (ClassNotFoundException e) {
//
}

FileClassLoader 类的实例将 java.lang.String 的加载委托给系统类加载器,因此,java.lang.String 是由系统类加载器加载和定义的,即使加载是由 FileClassLoader 发起的。

Definition 2.1: 假设 C 是 L.defineclass() 的结果,则 L defining loader C,或者等价地,L 定义 C。

Definition 2.2: 假设 C 是 L.loadClass() 的结果,则 L initiating loader C,或者等价的,L 初始加载 C。

在 Java 虚拟机中,每个类 C 都与它的 defining loader 永久关联,正是 C 的 defining loader 启动了 C 引用的任何类的加载。

关于 defining loader 和 initiating loader : 我们知道,在 java 中,类加载默认是双亲委派机制,那么基于此,来简单聊下 defining loader 和 initiating loader。

假设委托层次结构为 L-> Lp -> Lq,类在 Lp 中定义,在这种情况下:

  • L 将类加载委托给 Lp
  • Lp 将类加载委托给 Lq
  • Lq 不会加载该类,调用将返回到 Lp
  • Lp 将加载这个类,因为它是在 Lp 中定义的,并且调用返回到 L

在这里,Lp 和 L 都是 initiating class loaders(初始类加载器),Lp 是 defining class loader

类似地,如果委托等级是 L-> Lp,并且类在 L 中定义

  • L 成为定义和初始化类加载器。
  • Lp 不是初始化类加载器。

简单地说,如果类加载器能够返回对委托链中的类实例的引用,那么它就是初始化类加载器。即初始类加载器可能会有多个,但是 defining class loader 有且只有一个。

3 应用程序中的 Class Loaders

在本节中,我们将通过几个示例演示类装入器的强大功能。

重新加载类(Reloading Classes)

通常需要在诸如服务器之类的长期运行的应用程序中升级软件组件,升级不得要求应用程序关闭并重新启动。
在 Java 平台上,此功能可以转换为重新加载已经在运行的虚拟机中加载的类的子集,它对应于模式演化问题,通常可能很难解决,一些困难如下:

  • 可能有一些活动对象是我们想要 reload 的类的实例,那么就需要迁移这些对象,以符合新类的模式。例如,如果类的新版本包含一组不同的实例字段,我们必须以某种方式将现有的一组实例字段值映射到新版本类中的字段。
  • 将静态字段值映射到 reload 的类版本中的一组不同的静态字段。
  • 应用程序可能正在执行一个方法,该方法可能是我们想要 reload 的类。

在本文中,我们不讨论这些问题。但是我们将展示如何使用类加载器绕过它们,也就是通过在单独的类加载器中组织这些软件组件,开发者通常可以避免处理架构演变,那也就意味着,新类要由单独的加载器加载。

图 3 说明了 Server 类如何动态地将服务请求重定向到 Server 的新版本。关键技术是将 Server 类、旧 Service 和新 Service 加载到单独的类加载器中。例如,我们可以使用上一节中介绍的 FileClassLoader 类来定义 Server。
image.png

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
public class Server {

private Object service;

public void updateService(String location) {
try {
FileClassLoader cl = new FileClassLoader(location);
Class c = cl.loaderClass("Service");
service = c.newInstance();
} catch (ClassNotFoundException e){
// ignore
} catch (IllegalAccessException e){
// ignore
} catch (InstantiationException e){
// ignore
}

}

public void processRequest(String args) {
try {
Class c = service.getClass();
Method m = c.getMethod("run",String.class);
m.invoke(service,args);
} catch (NoSuchMethodException e){
// ignore
} catch (InvocationTargetException e){
// ignore
} catch (IllegalAccessException e){
// ignore
}
}
}

Server.processRequest 方法将所有传入请求重定向到存储在私有字段中的 service 对象。它使用 Java 核心反射 API 调用 service 对象上的“run”方法。另外,Server.updateService 方法允许动态加载 Service 类的新版本以替换现有的 Service 对象。updateService 的调用者提供新类文件的位置。进一步的请求将被重定向到 Service 引用的新对象。为了重新加载,Server 直接引用 Service 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Server {
private Service service;
public void updateService(String location) {
try {
FileClassLoader cl = new FileClassLoader(location);
Class c = cl.loaderClass("Service");
service = (Service) c.newInstance();
} catch (ClassNotFoundException e){
// ignore
} catch (IllegalAccessException e){
// ignore
} catch (InstantiationException e){
// ignore
}
}
// ..
}

一旦 Server 类将符号引用解析为 Service 类,它将包含到该类类型的强链接(hard linke),无法更改已解析的引用,这对于从类加载器返回的新版本的 Service,Server.updateService 方法最后一行中的类型转换将失败。

反射允许 Server 类在没有直接引用的情况下使用 Service 类。或者,Server 和 Service 可以共享一个公共接口或超类:

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
public class Server {
private ServiceInterface service;
public void updateService(String location) {
try {
FileClassLoader cl = new FileClassLoader(location);
Class c = cl.loaderClass("Service");
service = (ServiceInterface) c.newInstance();
} catch (ClassNotFoundException e){

} catch (IllegalAccessException e){

} catch (InstantiationException e){

}
}

public void processRequest(String args) {
service.run(args);
}
}


public class Service implements ServiceInterface{

@Override
public void run(String args){
System.out.println(args);
}
}

通过接口进行分派通常比反射更有效,不能重新加载接口类型本身,因为 Server 类只能引用一个 ServiceInterface 类型,getServiceClass 方法每次都必须返回一个实现相同 ServiceInterface 的类。

在我们调用 updateService 方法之后,所有未来的请求将由新的 Server 处理。然而,旧的 Server 类可能还没有完成对一些早期请求的处理。因此,两个 Server 类可能会共存一段时间,直到完成对旧类的所有使用,删除对旧类的所有引用,卸载旧类为止。

检测 Classes Files(Instrumenting Classes Files)

类加载器可以在进行 defineClass 调用之前检测类文件。例如,在 FileClassLoader 的例子中,我们可以插入一个调用来检测类文件的内容:

1
2
3
4
5
6
7
8
9
try {
byte[] data = getClassData(directory,name);
// 检测 classFile
byte[] newdata = instrumentClassFile(data);
return defineClass(name,newdata,0,newdata.length);
} catch (IOException e){
// keep looking
throw new ClassNotFoundException();
}

根据 Java 虚拟机规范,instrumented 类文件必须有效。虚拟机会将所有常规检查(例如运行字节码验证程序)应用于已检测的类文件,只要遵守类文件格式,程序员在修改类文件时就具有很大的自由度。例如,instrumented 类文件可能在现有方法,新字段或新方法中包含新的字节码指令,也可以删除现有方法,但是生成的类文件可能不会与其他类链接。

instrumented 的类文件必须定义一个与原始类文件名称相同的类。 loadClass 方法应返回其名称与作为参数传入的名称相匹配的类对象。

类加载器只能检测其定义的类,而不能委派给其他加载器的类。所有用户定义的类加载器应首先委托给系统类加载器,因此不能通过类加载器来

instrument 系统类。用户定义的类加载器无法通过尝试自己定义系统类来绕过此限制。例如,如果类加载器定义了自己的 String 类,则无法将该类的对象传递给需要标准 String 对象的 Java API。虚拟机将捕获并报告这些类型错误。

在许多情况下,类文件检测都是有用的。例如,一个已检测的类文件可能包含性能分析钩子,这些性能钩子方法计算特定方法的执行次数。通过用对某些类的引用替换对那些类的有资源意识的版本的引用,可以监视和控制资源分配。可以使用类加载器来实现参数化的类,为参数类型的每个不同调用扩展和定制类文件中的代码。

4 维护类型安全链接

到目前为止提供的示例已经证明了多个委托类加载器的用处。但是,正如我们将看到的,要确保在存在类加载器的情况下进行类型安全的链接需要特别注意。 Java编程语言依赖于基于名称的静态类型。在编译时,每种静态类类型都对应一个类名称。在运行时,类加载器会引入多个名称空间。运行时类类型不仅由其名称单独确定,还由一对:其类名称及其定义的类加载器确定。因此,用户定义的类加载器引入的名称空间可能与Java编译器管理的名称空间不一致,从而危害类型安全。

时间命名空间一致性(Temporal Namespace Consistency)

oadClass 方法可能会在不同时间为给定名称返回不同的类类型。为了维护类型安全,虚拟机必须能够始终为给定的类名和加载器获得相同的类类型。例如,考虑以下代码中对类X的两个引用:

1
2
3
4
5
class C {
void f(X x){...}
...
void g(){f(new x());}
}

如果 C 的类加载器将 X 的两次出现映射到不同的类类型,则 g 内部对 f 的方法调用的类型安全性将受到损害。虚拟机不能相信任何用户定义的loadClass 方法会对给定的名称一致地返回相同的类型,所以它在内部维护一个已加载的类缓存。被加载的类缓存将类名和初始加载器映射到类类型。当虚拟机从 loadClass 方法获得一个类后,它执行以下操作:

  • 将根据传递给 loadClass 方法的名称检查类的真实名称。如果 loadClass 返回的类没有所请求的名称,则会引发错误。
  • 如果名称匹配,则将生成的类缓存在已加载的类缓存中。虚拟机永远不会在同一类加载器上多次调用具有相同名称的 loadClass 方法

委托类加载命名空间一致性(Namespace Consistency among Delegating Loaders)

我们现在描述委派类装入器可能产生的类型安全问题

Notation 4.1:下面会会用 <C, Ld> Li 这样一个符号来代表一个类类型,其中 C 表示类名,Ld 表示类的 defining loader,Li 表示 loading loader。如果我们不关心 defining loader 时,我们使用 C Li 这个符号去表示 Li 是 C 的初始化加载器。当我们不关心初始化加载器时,我们使用指定的 <C, Ld> 去表示 C 是被 Ld定义的。

如果 L1 委托 L2 去加载 C,则 C:L1 = C:L2
现在我们将给出一个演示类型安全问题的示例。为了明确涉及到哪些类装入器,我们在通常出现类名的地方使用了上述表示法。

image.png

C 由 L1 定义。结果,L1 用于启动 C.f 内部引用的 Spoofed 和 Delegated 类的加载。 L 定义 Spoofed。但是,L1 将 Delegated 的加载委托给 L2,然后由 L2 定义 Delegated。由于 Delegated 由 L2 定义,因此 Delegated.g 将使用 L 来启动 Spoofed 的加载。碰巧的是,L2定义了另一种 Spoofed 类型。C 期望 <Spoofed,L1>的实例由 Delegated.g 返回。但是,Delegated.g 实际上返回<Spoofed,L2> 的实例,这是一个完全不同的类。

这是 L1 和 L2 的命名空间之间的不一致。如果未发现这种不一致,则可以使用委派的类加载器将一种类型伪造为另一种类型。若要了解这种类型的安全问题如何导致不良行为,请假设以下两个版本的“Spoofed”定义如下:

image.png

现在,类<C,L1>可以显示<Spoofed,L2>实例的私有字段,并从整数值伪造一个指针:

image.png

我们可以在<Spoofed,L2>实例中访问私有字段秘密值,因为该字段在<Spoofed,L1>中声明为公共字段。我们还能够在<Spoofed,L1>实例中将整数字段伪造为整数数组,并取消引用从该整数伪造的指针。
类型安全问题的根本原因是虚拟机没有考虑到类类型是由类名和定义加载器确定的。相反,虚拟机依赖于Java编程语言的概念,即在类型检查期间只使用类名作为类型。这个问题已经得到纠正,如下所述。

解决方案

类型安全问题的一个直接解决方案是在Java虚拟机中统一使用类名及其定义加载器来表示类类型。但是,确定定义加载器的唯一方法是通过初始加载器实际加载类。在前一节的例子中,在确定 C.f 对 Delegated.g 的调用是否是类型安全的之前,我们必须首先在 L1和L2中都加载 Spoofed,然后查看是否获得相同的定义加载程序。这种方法的缺点是它牺牲了类的延迟加载性质。

我们的解决方案保留了简单方法的类型安全性,但避免了急切的类加载。关键思想是维护一组加载器约束,在类加载发生时动态更新这些约束。在上面的例子中,我们没有在 L 1和 L2 中加载 Spoofed,而是简单地记录了一个约束:Spoofed:L1=Spoofed:L2。如果 Spoofed 稍后被 L1或 L 2加载,我们将需要验证现有的加载器约束集不会被违反。如果约束 Spoofed:L1=Spoofed:L2是在 L1 和 L2加载 Spoofed 之后引入的,会怎么样?现在施加约束并撤消以前的类加载已经太晚了。

因此,我们必须同时考虑加载的类缓存和加载器约束设置。我们需要保持不变:加载的类缓存中的每个条目都满足加载器的所有约束。保持不变式如下:

  • 每次将一个新条目添加到已加载的类缓存中时,我们都要验证没有违反任何现有的加载器约束。如果不能在不违反现有加载器约束的情况下将新条目添加到加载的类缓存中,则类加载失败。
  • 每次添加新的加载器约束时,我们都要验证缓存中加载的所有类是否满足新的约束。如果一个新的加载器约束不能被所有加载的类满足,那么触发添加新的加载器约束的操作将失败。

让我们看看如何将这些检查应用到前面的示例中。C.f 方法的第一行使虚拟机生成约束 Spoofed:L1=Spoofed:L2。如果 L1和 L2 在我们生成这个约束时已经加载了 Spoofed 类,那么一个异常将立即在程序中引发。否则,约束将被成功记录。假设 Delegated.g 首先加载 Spoofed:L2,当稍后C.f 尝试加载 Spoofed:L1 时,将引发一个异常。

约束规则

现在我们声明生成约束的规则。这些对应于一个类类型可能被另一个类引用的情况。当在不同的加载器中定义两个这样的类时,名称空间之间可能存在不一致。

  • 如果 <C, L1> 引用了一个字段 : T filedname, 这个字段是在 <D,L2> 中被申明,那我们就产生了这样一个约束:T:L1 = T:L2
  • 如果 <C, L1> 引用了一个方法 :T0 methodname(T1 ,….., Tn); 这个方法在 <D,L2> 中被申明,那我们就产生了一个约束:T0:L1 = T0:L2,…, Tn:L1 = Tn:L2
  • 如果 <C, L1> 重写了一个方法:T0 methodname(T1 ,….., Tn);这个方法在 <D,L2> 中被申明,那我们就产生了一个约束:T0:L1 = T0:L2,…, Tn:L1 = Tn:L2

约束集{T:L1= T:L2, T:L2 = T:L3}表示在 L1和 L2、L 2和 L3 中,T 必须以相同的类类型加载。即使在程序执行过程中,T 从未被 L2 加载,L 1和 L2 也无法加载不同版本的 T。如果违反了加载程序约束,则将抛出 java.lang.LinkageError 异常。当相应的类加载器被垃圾回收时,加载器约束将从约束集中删除。

替代解决方案

另外一种与前不同的替代方案是,建议方法重写也应该基于动态类型,而不是基于静态(基于名称)类型。这种方案是从链接时间开始统一地使用类型的动态概念。下面的代码说明了该模型和前面提到的模型之间的差异:

1
2
3
class<Super,L1> {
void f(Spoofed x){... code1....}
}

image.png

假设 L1 和 L2 定义了不同版本的 Spoofed。Saraswat 认为 Super 和 Sub 中的 f 方法具有不同的类型签名 :Super.f 方法参数类型是<Spoofed,L1>, 而 Sub.f 方法参数类型是 <Spoofed,L2>。这样的话,Sub.f 在这个模型下就没有办法去 override Super.f 方法。

在我们前面设定的模型中,如果 Main 是被 L2 加载的,当 f 被调用时,就会产生 linkageError 异常。这个行为和替代模型是非常相似的:在替代模型中会产生 NoSuchMethodError。

在我们的模型中,当 Main 由 L1加载时,方法上的差异变得明显,当 Main 由 L 1加载时,对f的调用将调用 code2。当代码 2 尝试访问 Spoofed 的任何字段或方法时,将引发 linkageError。
我们认为,在这种情况下失败比静默运行本不应该执行的代码更好。程序员在编写上面的 Super 和 Sub 类时,期望 Sub.f 确实根据 Java 编程语言的语义覆盖了 Super.f。这些期望在替代模型的提议中被违反了。

结论

我们已经提出了Java平台中的类加载器的概念。类加载器结合了四个理想的功能:延迟加载,类型安全的链接,多个名称空间和用户可扩展性。特别是类型安全,需要特别注意。我们已经展示了如何在不限制类加载器功能的情况下保持类型安全。类加载器是一种简单而强大的机制,已被证明在管理软件组件方面非常有价值。

作者

卫恒

发布于

2020-12-11

更新于

2022-04-24

许可协议

评论