类加载机制

  • 一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历 加载(Loading)验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为 连接(Linking)。这七个阶段的发生顺序如图所示。

这七个阶段按序开始,但不意味着一个阶段结束另一个阶段才能开始。也就是说,不同的阶段往往是穿插着进行的,加载阶段中可能会激活验证的开始,而验证阶段又有可能激活准备阶段的赋值操作等,但整体的开始顺序是不会变的。

加载

加载就是怎么样在拿到字节流的过程。

在加载阶段,Java 虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的 静态存储结构转化为方法区(Method Area)的运行时数据结构
  3. 在堆内存(Heap)中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

首先,第一个过程,读取字节码文件进入内存。具体如何读取,虚拟机规范中并没有明确指明。也就是说,你可以从 ZIP 包中读取,也可以从网络中获取,还可以动态生成,或者从数据库中读取等,反正最终得到的结果一样:字节码文件的二进制流

第二步,将这个内存中的二进制流重新编码存储,依照方法区的存储结构进行存储,方便后续的验证和解析。方法区数据结构如下:

大体上的格式和我们虚拟机规范中的 Class 文件格式是差不多的,只是这里增加了一些项,重排了某些项的顺序。

第三步,生成一个 java.lang.class 类型的对象。这个类型的对象创建的具体细节,我们不得而知,但是这个对象存在于方法区之中的唯一目的就是,唯一表示了当前类的基本信息,外部所有该类的对象的创建都是要基于这个 class 对象的,因为它描述了当前类的所有信息。

可见,整个加载阶段,后两个步骤我们不可控,唯一可控的是第一步,加载字节码。具体如何加载,这部分内容,这里不打算详细说明,具体内容将于下文描述「类加载器」时进行说明。

验证

验证就是验证拿到的这串字节流是否符合 Java 语言和 Java 虚拟机的规范。验证阶段的目的是为了确保加载的 Class 文件中的字节流是符合虚拟机运行要求的,不能威胁到虚拟机自身安全。

这个阶段「把控」的如何,将直接决定了我们虚拟机能否承受住恶意代码的攻击。整个验证又分为四个阶段:文件格式验证、元数据验证、字节码验证,符号引用验证

文件格式验证

这个阶段将于「加载」阶段的第一个子阶段结束后被激活,主要对已经进入内存的二进制流进行判断,是否满足 虚拟机规范中要求的 Class 文件格式。例如:

  1. 验证 class 魔数的值是否为:0xCAFEBABE
  2. 主次版本号是否在当前虚拟机处理范围之内
  3. 检查常量池中的各项常量是否为常量池所支持的类型(tag 字段是否异常取值)
  4. 常量项 CONSTATNT_Utf8_info 中存储的字面量值是否不符合 utf8 编码标准
  5. 等等等等

元数据验证

该阶段的验证主要针对字节码文件所描述的语义进行验证,验证它是否符合 Java 语言规范 的要求。例如:

  • 这个类是否有父类,Object 类除外
  • 这个类是否继承了某个不允许被继承的类
  • 这个类中定义的方法,字段是否存在冲突
  • 等等等等

虽然某些校验在编译器中已经验证过了,这里却依然需要验证的原因是,并不是所有的 Class 文件都是由编译器产生的,也可以根据 Class 文件格式规范,直接编写二进制得到。虽然这种情况少之又少,但是不代表不存在,所以这一步的验证的存在是很有必要的。

字节码验证

经过「元数据验证」之后,整个字节码文件中定义的语义必然会符合 Java 语言规范。但是并不能保证方法内部的字节码指令能够很好的协作,比如出现:跳转指令跳转到方法体之外的字节码指令上,字节码指令取错操作数栈中的数据等问题

这部分的验证比较复杂,我查了很多资料,大部分都一带而过。总体上来说,这阶段的验证主要是对类的方法体字节码指令(Class 文件中的 Code 属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为 。

在 JDK 6 之后的 Javac 编译器和 Java 虚拟机里进行了一项联合优化,把尽可能多的校验辅助措施挪到 Javac 编译器里进行。具体做法是给方法体 Code 属性的属性表中新增加了一项名为“StackMapTable”的新属性,这项属性描述了方法体所有的基本块(Basic Block,指按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,在字节码验证期间,Java 虚拟机就不需要根据程序推导这些状态的合法性,只需要检查 StackMapTable 属性中的记录是否合法即可。这样就将字节码验证的类型推导转变为类型检查,从而节省了大量校验时间。

理论上 StackMapTable 属性也存在错误或被篡改的可能,所以是否有可能在恶意篡改了 Code 属性的同时,也生成相应的 StackMapTable 属性来骗过虚拟机的类型校验,则是虚拟机设计者们需要仔细思考的问题。

符号引用验证

这个验证相对而言就比较简单了,它发生在「解析」阶段之中。当「解析」阶段开始完成一个符号引用类型的加载之后,符号引用验证将会被激活,针对常量池中的符号引用进行一些校验。比如:

  • CONSANT_Class_info 所对应的类是否已经被加载进内内存了
  • 类的相关字段,方法的符号引用是否能得到对应
  • 对类,方法,字段的访问性是否能得到满足
  • 等等等等

符号引用验证通过之后,解析阶段才能继续。

总结

验证阶段总共分为四个子阶段,任意一个阶段出现错误,都将抛出 java.lang.VerifyError 异常或其子类异常。当然,如果你觉得验证阶段会拖慢你的程序,jvm 提供:-Xverify:none 启动参数关闭验证阶段,缩短虚拟机类加载时间。

准备

准备阶段实际上是为 类变量(即静态变量,被 static 修饰的变量) 赋「系统初值」的过程,这里的「系统初值」并不是指通过赋值语句初始化变量的意思,基本数据类型的零值,如下:

  • 对于整数类型(byte、short、int、long),零值是 0/0/0/0l。

  • 对于浮点数类型(float、double),零值是 0.0f /0.0d。

  • 对于字符类型(char),零值是空字符 \u0000

  • 对于布尔类型(boolean),零值是 false。

  • 对于引用类型(类、接口、数组、枚举),零值是 null。

1
2
3
4
5
6
7
public static int n = 2;
// 初始化值是 0,而不是 2,因为这个时候还没有执行任何 java 方法
// 而把 value 赋值为 2 的 public static 指令是在程序编译后,存放于类构造器 <clinit> 方法之中的,所以把 value 赋值为 3 的动作将在初始化阶段才会执行。

static final int n = 2;
// 编译时 Javac 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 3。
// 我们可以理解为 static final 常量在编译期就将其结果放入了调用它的类的常量池中

还需要注意如下几点:

  1. 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
  2. 对于同时被 static 和 final 修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被 final 修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  3. 对于引用数据类型 reference 来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即 null。
  4. 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

解析

解析阶段是 Java 虚拟机将常量池内的 符号引用替换为直接引用 的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号进行,分别对应于常量池的 CONSTANT_Class_info、CON-STANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_M ethodHandle_info、CONSTANT_Dyna-mic_info 和 CONSTANT_InvokeDynamic_info 8 种常量类型。

原先,在我们 Class 文件中的常量池里面,存在两种类型的常量,直接字面量(直接引用)和符号引用。直接引用指向的是具体的字面量,即数字或者字符串。而符号引用存储的是对直接引用的描述,并不是指向直接的字面量。例如我们的 CONSTANT_Class_info 中的 name_index 存储就是对常量池的一个偏量值,而不是直接存储的字符串的地址,也就是说,符号引用指向直接引用,而直接引用指向具体的字面量。

  • 符号引用:比如引用一个类,java.util.ArrayList 这就是一个符号引用,字符串引用的对象不一定被加载。
  • 直接引用: 就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。引用对象一定在内存(已经加载)。

为什么要这样设计,其实就是为了共用常量项。 如果不是为了共享常量,我也可以定义 name_index 后连续两个字节用来表述类的全限定名的 utf8 编码,只不过一旦整个类中有多个重复的常量项的话,就显得浪费内存了。

当一个类被加载进方法区之后,该类的常量池中的所有常量将会入驻方法区的运行时常量池。这是一块对所有线程公开的内存区域,多个类之间如果有重复的常量将会被合并。直接引用会直接入驻常量池,而符号引用则需要通过解析阶段来实际指向运行时常量池中的直接引用的地址。

这就是解析阶段所要完成的事情,下面我们具体看看不同的符号引用是如何被翻译成直接引用的。

类或接口的解析

假设当前代码所处的类是 A,在 A 中遇一个从未解析过的符号引用 N 解析为一个类或接口 C 的直接引用,那虚拟机完成整个解析的过程需要包括以下 3 个步骤:

  1. 通过常量池找到 B 这个符号引用所对应的直接引用(类的全限定名的 utf8 编码)
  2. 把这个全限定名称传递给 A 的类加载器完成类加载(包括我们完整的七个步骤),如果没有出现任何异常,那么 C 在虚拟机中实际上已经成为一个有效的类或接口了
  3. 但在解析完成前还要进行符号引用验证,确认 A 否具备对 C 的访问权限。如果发现不具备访问权限,将抛出 java.lang.llegalAccessError 异常。验证成功后替换符号引用 N 的值为内存中刚加载的类或者接口的地址
  • 当然,对于我们的数组类型是稍有不同的,因为数组类型在运行时由 jvm 动态创建,所以在解析阶段的第一步,jvm 需要额外去创建一个数组类型放在常量池中,其余步骤基本相同。

  • 如果 C 是一个数组类型,并且数组的元素类型为对象,也就是 N 的描述符会是类似“[Ljava/lang/Integer”的形式,那将会按照第一点的规则加载数组元素类型。如果 N 的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表该数组维度和元素的数组对象。

针对上面第 3 点访问权限验证,在 JDK9 引入了模块化以后,一个 public 类型也不再意味着程序任何位置都有它的访问权限,我们还必须检查模块间的访问权限。如果我们说一个 D 拥有 C 的访问权限,那就意味着以下 3 条规则中至少有其中一条成立:

  • 被访问类 C 是 public 的,并且与访问类 D 处于同一个模块。
  • 被访问类 C 是 public 的,不与访问类 D 处于同一个模块,但是被访问类 C 的模块允许被访问类 D 的模块进行访问。
  • 被访问类 C 不是 public 的,但是它与访问类 D 处于同一个包中。
  • 在后续涉及可访问性时,都必须考虑模块间访问权限隔离的约束,即以上列举的 3 条规则,这些内容在后面就不再复述了。

字段的解析

字段在常量池中由常量项 Fieldref 描述,解析开始时,首先会对字段表内 class_index 项中索引的 CONSTANT_Class_info 符号引用进行解析,也就是字段所属的类或接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。这部分内容实在很抽象,很多资料都没有明确说明,字段的符号引用最后会指向哪里。我的理解是,常量池中的字段项会指向类文件字段表中某个字段的首地址(纯属个人理解)。

解析过程如下。如果顺利将会得到字段所属的类 C,接下来的解析过程如下:

  • 通过字段项 nameAndType 查找 A 中是否有匹配的项(简单名称和字段描述符都与目标相匹配),如果有则直接返回该字段的引用。

  • 如果没有,递归向上搜索 A 实现的所有接口去匹配。

  • 如果还是未能成功,向上搜索 A 的父类

  • 若依然失败,抛出 java.lang.NoSuchFieldError 异常

  • 如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出 java.lang.IllegalAccessError 异常。

以上解析规则能够确保 Java 虚拟机获得字段唯一的解析结果,但在实际情况中,Javac 编译器往往会采取比上述规范更加严格一些的约束,譬如有一个同名字段同时出现在某个类的接口和父类当中,或者同时在自己或父类的多个接口中出现,按照解析规则仍是可以确定唯一的访问字段,但 Javac 编译器就可能直接拒绝其编译为 Class 文件。

方法解析

方法解析的第一个步骤与字段解析一样,也是需要先解析出方法表的 class_index 项中索引的方法所属的类或接口的符号引用,如果解析成功,那么我们依然用 C 表示这个类,接下来虚拟机将会按照如下步骤进行后续的方法搜索:

  1. 由于 Class 文件格式中类的方法和接口的方法符号引用的常量类型定义是分开的,如果在类的方法表中发现 class_index 中索引的 C 是个接口的话,那就直接抛出 java.lang.IncompatibleClassChangeError 异常。

  2. 如果通过了第一步,在类 C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

  3. 否则,在类 C 的父类中递归查找 是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

  4. 否则,在类 C 实现的接口列表及它们的父接口之中递归查找 是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类 C 是一个抽象类,这时候查找结束,抛 java.lang.AbstractMethodError 异常。

  5. 否则,宣告方法查找失败,抛出 java.lang.NoSuchMethodError。

  6. 最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出 java.lang.IllegalAccessError 异常。

接口方法解析

接口方法也是需要先解析出接口方法表的 class_index 项中索引的方法所属的类或接口的符号引用,如果
解析成功,依然用 C 表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索:

  1. 与类的方法解析相反,如果在接口方法表中发现 class_index 中的索引 C 是个类而不是接口,那么就直接抛出 java.lang.IncompatibleClassChangeError 异常。
  2. 否则,在接口 C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
  3. 否则,在接口 C 的父接口中递归查找,直到 java.lang.Object 类(接口方法的查找范围也会包括 Object 类中的方法)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  4. 对于规则 3,由于 Java 的接口允许多重继承,如果 C 的不同父接口中存有多个简单名称和描述符都与目标相匹配的方法,那将会从这多个方法中返回其中一个并结束查找,《Java 虚拟机规范》中并没有进一步规则约束应该返回哪一个接口方法。但与之前字段查找类似地,不同发行商实现的 Ja-vac 编译器有可能会按照更严格的约束拒绝编译这种代码来避免不确定性。
  5. 否则,宣告方法查找失败,抛出 java.lang.NoSuchMethodError 异常。

在 JDK9 之前,Java 接口中的所有方法都默认是 public 的,也没有模块化的访问约束,所以不存在访问权限的问题,接口方法的符号解析就不可能抛出 java.lang.IllegalAccessError 异常。

  • 在 JDK9 中增加了接口的静态私有方法,也有了模块化的访问约束,所以从 JDK9 起,接口方法的访问也完全有可能因访问权限控制而出现 java.lang.IllegalAccessError 异常。

初始化

之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由 Java 虚拟机来主导控制。直到初始化阶段,Java 虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。

和准备阶段所做的事情截然不同,准备阶段只是为所有类变量赋系统初值,而初始化阶段才会执行我们的程序代码(仅限于类变量的赋值语句)。编译器会在编译的时候收集类中所有的 静态语句块和静态赋值语句 合并到一个方法中,然后我们的虚拟机在初始化阶段只要调用这个方法就可以完成对类的初始化了。

准备阶段: 在准备阶段,所有类变量(即静态变量)都会被分配默认值。对于基本类型,这个默认值是 0,对于对象引用,默认值是 null。这个阶段的目的是为了确保类变量在使用之前有一个初始值。

初始化阶段: 在初始化阶段,类的静态变量会按照代码中的 赋值语句进行初始化。这包括静态代码块和静态变量的显式赋值。

  • 进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。

在 Java 中,<init><clinit> 是与类的初始化相关的两个特殊方法,它们在 JVM 中起着重要的作用。总结起来,在 JVM 中,<clinit> 方法是与类相关的静态初始化方法,而 <init> 方法是与对象实例相关的初始化方法。

  1. <init> 方法:这是实例初始化方法,用于初始化对象实例。当你创建一个类的新对象时,该类的构造方法(可能是默认构造方法或自定义构造方法)会被调用。在字节码中,这个构造方法的名称是 <init>
  2. <clinit> 方法:这是类初始化方法,用于执行类的静态初始化。静态初始化是在类加载时进行的,它包括对静态成员变量的赋值和执行静态块中的代码。在字节码中,这个静态初始化方法的名称是 <clinit>

在 JVM 加载类的过程中,首先执行类的静态初始化(如果有的话,即执行 <clinit> 方法),然后才创建对象并执行实例初始化(即执行 <init> 方法)。这确保了静态成员变量在对象创建之前得到正确的初始化。

不能向后访问

  • 我们也可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器 <clinit> 方法的过程。<clinit> 并不是程序员在 Java 代码中直接编写的方法,它是 Javac 编译器的自动生成物,但我们非常有必要了解这个方法具体是如何产生的,以及 <clinit> 方法执行过程中各种可能会影响程序运行行为的细节,这部分比起其他类加载过程更贴近于普通的程序开发人员的实际工作。

  • <clinit> 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

clinit 父类的调用

  • <clinit> 方法与类的构造函数(即在虚拟机视角中的实例构造器 <init>()方法不同,它不需要显式地调用父类构造器,Java 虚拟机会保证在子类的 <clinit> 方法执行前,父类的 <clinit> 方法已经执行完毕。因此在 Java 虚拟机中第一个被执行的 <clinit> 方法的类型肯定是 java.lang.0bject。

  • 由于父类的 <clinit> 方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如代码中,字段 B 的值将会是 2 而不是 1。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class Test01 {
    static class Parent{
    public static int A = 1;
    static {
    A = 2;
    }
    }
    static class Sub extends Parent{
    public static int B = A; // 静态语句块要优先于子类的变量赋值
    static {
    B = 2;
    }
    }
    public static void main(String[] args) {
    System.out.println(Parent.A); // 2
    System.out.println(Sub.B); // 2
    }
    }

clinit 非必须

  • <clinit>() 方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 <clinit>() 方法;
  • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成 <clinit> 方法。但接口与类不同的是,执行接口的 <clinit> 方法不需要先执行父接口的 <clinit> 方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的 <clinit> 方法。

clinit 中执行一次

  • Java 虚拟机必须保证一个类的 <clinit> 方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的 <clinit> 方法,其他线程都需要阻塞等待,直到活动线程执行完毕 <clinit> 方法。如果在一个类的 <clinit> 方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

    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
    package pers.fulsun;

    public class Test01 {
    static class DeadLoopClass {

    static {
    if (true) {
    System.out.println(Thread.currentThread() + "static statement ");
    while (true) {

    }
    }
    }
    }


    public static void main(String[] args) {
    Runnable runnable = () -> {
    System.out.println(Thread.currentThread() + "start");
    DeadLoopClass deadLoopClass = new DeadLoopClass();
    System.out.println(Thread.currentThread() + "over");
    };
    Thread thread = new Thread(runnable);
    Thread thread2 = new Thread(runnable);
    thread.start();
    thread2.start();
    }
    }

    Thread[Thread-1,5,main]start
    Thread[Thread-0,5,main]start
    Thread[Thread-1,5,main]static statement
    // 阻塞第二个线程


    //如果注释死循环,可以发现只会执行一次
    Thread[Thread-1,5,main]start
    Thread[Thread-0,5,main]start
    Thread[Thread-1,5,main]static statement
    Thread[Thread-1,5,main]start
    Thread[Thread-0,5,main]start

经典的面试题:

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
class SingleTon {
private static SingleTon singleTon = new SingleTon();
public static int count1;
public static int count2 = 0;

private SingleTon() {
count1++;
count2++;
}

public static SingleTon getInstance() {
return singleTon;
}
}

public class Test {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}
}

// 解析说明
1. Test 类会被第一个加载,然后程序开始执行 main 方法的字节码。
2. 遇到 SingleTon 这个类,检索了一下方法区,发现没有被加载,于是开始加载 SingleTon类:
第一步,将 SingleTon 这个类的字节码文件加载进方法区,经过文件格式验证,这个字节码文件顺利转储为方法区的数据结构
第二步,继续进行元数据验证,确保字节码文件中的语义合法,接着字节码验证,保证方法中的字节码指令之间不存在异常
第三步,准备阶段,开始为类变量赋系统初值,本例中 singleTon = null,count1 = 0,count2 = 0
第四步,将类常量池中的直接引用入驻方法区运行时常量池,接着解析符号引用到具体的直接引用
第五步,执行类变量的初始化语句。
类的静态变量会按照代码中的赋值语句进行初始化。
0. count1 和 count2 在准备阶段被赋予默认值0
1. 这里类变量 singleTon 会被赋值为一个对象的引用,初始化时执行init方法, count1 = 1,count2 = 1
2. 静态变量的显式赋值 将 count 2 = 0
3. 到此,类变量 singleton 初始化完成,打印结果
count1=1
count2=0

类加载器

类加载的第一步就是将一个二进制字节码文件加载进方法区内存中,而这部分内容我们前文并没有详细说明,接下来我们就来看看如何将一个磁盘上的字节码文件加载进虚拟机内存中。

类加载器负责加载类及其依赖的其他类。当一个类被某个类加载器加载时,它所依赖的其他类也会由相同的类加载器来加载。这种加载方式有助于保持类与其依赖之间的一致性,避免不同类加载器之间的冲突。

同一个类加载器加载的类之间是有相互可见性的,而不同类加载器加载的类之间是相互隔离的。这种隔离性有助于解决类版本冲突等问题。

类加载器的三个基本特征 :

  • 双亲委派模型
  • 可见性; 子类加载器可以访问父类加载器加载的类型, 反过来不允许,
  • 单一性; 父类加载过的类型, 子类加载器不会重复加载

类加载器主要分为四个不同类别

  • BootStrap ClassLoader(启动类加载器):该加载器由 C++语言实现,是虚拟机的一部分,主要负责加载 JDK\jre\lib\rt.jar 类文件。(文件很大)。Java 程序员不能直接使用该类加载器。
  • Extension ClassLoader(扩展类加载器) :Java 语言实现,独立于 JVM。主要加载\JDK\jre\lib\ext 的文件。
  • Application ClassLoder(应用程序类加载器):Java 语言实现,独立于 JVM。加载 ClassPath 下的指定类库。开发者可以直接使用的类加载器,若没有自定义类加器,一般情况就是程序的默认类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,
  • 用户自定义类加载器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Test{}
class TestClassLoader {
public static void main(String[] args) {
Class<Test> cls = Test.class;
System.out.println(cls.getClassLoader());
System.out.println("Classpath: " + System.getProperty("java.class.path"));
System.out.println(cls.getClassLoader().getParent());
// 最顶层的 BootStrap 类加载器有 C++实现,因此返回 null。
System.out.println(cls.getClassLoader().getParent().getParent());
}
}


// 结果
sun.misc.Launcher$AppClassLoader@18b4aac2
//Classpath: .... ; C:\Users\fulsun\Desktop\test1\target\classes;...
sun.misc.Launcher$ExtClassLoader@75b84c92
null

双亲委派模型

一个类加载器收到类加载的请求,首先不会自己去加载这个类,而是将这个请求委派给父加载器完成,每一层都这样,因此这个类加载请求会来到最高层,若父加载器可以加载这个类,父加载器优先加载。若不在父的加载范围之内或者父加载器加载不了,此时才由子类加载器加载。这个过程便是双亲委派模型。

它们之间的调用关系如下:

如果用户自定义一个 java.long.String 类,放在 ClassPath 下,则按照双亲委派模型的流程,交由最顶层的 BootStrap 加载器加载,因为在它的加载范围内有这个类,因此它会加载 JDK 中的 String 类,而自定义的 String 类不会被加载。换言之,如果没有双亲委派模型,自定义的 String 类,被 Application 加载,自己定义的 String 类,又没有 JDK 提供的 String 类的功能,而其它系统提供的类都使用了 String 这个类,这样会导致整个程序一片混乱。

简单而言:通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次,最主要的是防止恶意覆盖 Java 核心 API。

ClassLoader

ClassLoader 类的 loadClass 的实现:整体上来看这个 loadClass,你会发现它很巧妙的实现了「双亲委托」模型,而核心就是那段『捕获异常而什么都不做』的操作。

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
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

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;
}
}

在之前的 jdk 版本中,如果想要自定义类加载器来加载我们的 Class 文件,我们通过继承 ClassLoader 并重写其 loadClass 即可完成自定义的类加载器的具体实现。其中这个 loadClass 方法是 protected 修饰且没有加 final 的,因此用户可以通过覆写打破这个逻辑。

但是现在 jdk 1.8 已经不再推荐这么做了,具体我们一点一点来看。

  1. 首先明确一点,loadClass 方法的这个参数 name 指的是待加载类的全限定名称,例如:java.lang.String 。

  2. 然后第一步,调用方法 findLoadedClass 判断这个类是否已经被当前的类加载器加载。如果已经被当前的类加载器加载了,那么直接返回方法区中的该类型的 class 对象即可,否则返回 null

  3. 如果该类未被当前类加载器加载,那么将进入 if 的判断体中,这段代码即完成了「双亲委托」模型的实现。我们具体看一看:

    • 先拿到当前类加载器的父加载器,如果不是 null,那么传递当前类给父加载器加载去,接着会递归进入 loadClass。
    • 如果父加载器为 null,那么就启动 Bootstrap 启动类加载器进行加载。
  4. 如果上级的类加载器在自己负责的「目录范围」里,找不到传递过来待加载的类,那么会抛出 ClassNotFoundException 异常,而捕获异常后什么也没做,即当前调用结束。也就是说,下级类加载器请求上级类加载器加载某个类,而如果上级加载器不能加载,会导致此次调用安全结束。那么此时的 c 必然为 null。

  5. 这样的话,当前类加载器就会调用 findClass 方法自己去加载该类,而这个 findClass 的实现为空,换句话说,jdk 希望我们通过实现这个方法来完成自定义的类型加载,这样不会打破双亲委派模型。

自定义类加载器

假设在桌面放着一个 Hello.class 文件,下面我们自定义一个类加载器并加载任意一个类:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package pers.fulsun;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class MyClassLoad extends ClassLoader {

String targetpath = "C:\\Users\\fulsun\\Desktop\\";

@Override
protected Class<?> findClass(String name) {
String fileName = targetpath + name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = Files.newInputStream(Paths.get(fileName));
out = new ByteArrayOutputStream();
int len;
byte[] buffer = new byte[1024];
while ((len = in.read(buffer,0,buffer.length)) != -1) {
out.write(buffer, 0, len);
}
byte[] result = out.toByteArray();
return defineClass(name, result, 0, result.length);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
assert in != null;
in.close();
assert out != null;
out.close();
}catch (IOException e) {
e.printStackTrace();
}
}
return null;
}

//主函数调用
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoad classLoad = new MyClassLoad();
Class clazz1 = classLoad.loadClass("pers.fulsun.Hello");

System.out.println(clazz1.getClassLoader());
System.out.println(clazz1.getClassLoader().getParent());
}
}


// 结果
pers.fulsun.MyClassLoad@75b84c92
sun.misc.Launcher$AppClassLoader@18b4aac2
/// 如果使用 Idea target\classes 下的文件还是会交给 AppClassLoader 去加载
  • 自定义的 MyclassLoader 加载。它处于类加载器的最底层,它的父类加载器为 AppClassLoader.

  • 整体上来说,我们的 MyClassLoader 其实只干了一件事情,就是将磁盘文件读取进内存,保存在 byte 数组中,然后调用 defineClass 方法进行后续的类加载过程,这是个本地方法,我们看不到它的实现。

  • 换句话说,虽然 jdk 允许我们自定义类加载器加载字节码文件,但是我们能做的也只是读文件而已,底层的东西都被封装的好好的,后续等我们看 Hotspot 源码的时候再去剖析它的底层实现。

  • 一种类加载器总是负责某个范围或者目录下的所有文件的加载,就像 bootstrap 加载器负责加载 \lib 这个目录中存放的所有字节码文件,extenttion 加载器负责 \lib\ext 目录下的所有字节码文件,而 application 类加载器则负责我们项目 类路径 下的字节码文件的加载。

  • 至于自定义的类加载器而言,加载目录也随之自定义了,例如我们这里实现的类加载器则负责桌面目录下所有的 Class 文件的加载。

打破双亲委派模型

在一个 Tomcat 中是可以部署多个项目的,当这多个项目需要依赖相同的第三方 jar 包的不同版本的时候,如果采用 JDK 默认的类加载机制,自然是不能实现的,因为虽然它们的版本不一致,但是类的全路径是一样的,在加载的过程中,只会加载一次。因此 Tomcat 中引入了 webAppClassLoader,这个类加载器覆写了 loadClass 方法,在加载 classpath 的.class 文件时默认不会交给父加载器加载,自己加载完毕直接返回,因此打破了双亲委派模型。如图所示:

  • CommonClassLoader 加载:%TOMCAT_HOME%/common/ 中的 Java 类库;Tomcat 最基本的类加载器,加载路径中的 class 可以被 Tomcat 容器本身以及各个 Webapp 访问;

  • CatalinaClassLoader 加载:%TOMCAT_HOME%/server/ 中的 Java 类库;Tomcat 容器私有的类加载器,加载路径中的 class 对于 Webapp 不可见;

  • SharedClassLoader 加载:%TOMCAT_HOME%/shared/ 中的 Java 类库;各个 Webapp 共享的类加载器,加载路径中的 class 对于所有 Webapp 可见,但是对于 Tomcat 容器不可见;

  • WebApp ClassLoaderl 加载:%TOMCAT_HOME%/WebApp/AppName/WEB-INF/ 中的 Java 类库;各个 Webapp 私有的类加载器,加载路径中的 class 只对当前 Webapp 可见;

Tomcat 给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了 loadClass 方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找,这种情况下,确实会在内存中存在全路径一样的多个 class,但是它们的 classloader 不一样,因此也不会发生冲突。

并不是 Web 应用程序下的所有依赖都需要隔离的,比如 Redis 就可以 Web 应用程序之间共享(如果有需要的话),因为如果版本相同,没必要每个 Web 应用程序都独自加载一份啊, Tomcat 就在 WebAppClassLoader 上加了个父类加载器(SharedClassLoader),如果 WebAppClassLoader 自身没有加载到某个类,那就委托 SharedClassLoader 去加载。

为了隔绝 Web 应用程序与 Tomcat 本身的类,又有类加载器(CatalinaClassLoader)来装载 Tomcat 本身的依赖

如果 Tomcat 本身的依赖和 Web 应用还需要共享,那么还有类加载器(CommonClassLoader)来装载进而达到共享

SPI 的类加载机制

在 JDK 1.6 版本引入一个 SPI 机制,全称是 Service Provider Interface。SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,使用 java.util.ServiceLoader 类进行动态装载。

ServiceLoader:SPI 使用约定加载 class path:META-INF/接口全限定名 文件内容指定的类

说明

  • JDBC(Java Database Connectivity)并没有直接打破 Java 的双亲委派机制。双亲委派机制是 Java 类加载器的一种机制,用于保证 Java 类的安全性和一致性。
  • 之前我们加载数据库驱动程序可以使用 Class.forName 方法来加载驱动程序的实现类,而这个方法默认是使用调用者的类加载器来加载类。
  • 在 JDBC 4.0 后,规范中明确要求 Drive 类必须向 DriverManager 注册自己, 不在需要 Class.forName 来加载驱动程序。这里就存在了一些问题。
    1. DriverManager 和启动类加载器: DriverManager 类属于 Java 标准库,通常位于 rt.jar 中,是由启动类加载器加载的。启动类加载器负责加载 Java 核心库,而 rt.jar 就是其中之一。
    2. com.mysql.jdbc.Driver 的位置: 通常,数据库厂商提供的 JDBC 驱动类,比如 com.mysql.jdbc.Driver,不在 Java 核心库的路径下,而是由应用程序的类加载器加载的。这些驱动类通常以外部库的形式存在,而不是 Java 标准库的一部分。
    3. BootStrap ClassLoader 的限制: 由于类加载器的委托关系,父级加载器无法访问子级类加载器路径中的类。这意味着启动类加载器无法直接加载数据库厂商提供的 JDBC 驱动类,因为它们通常不在启动类加载器的加载路径下。
    4. 双亲委派模型的局限性: 双亲委派模型确保了类加载的一致性和安全性,但在某些情况下,例如加载厂商提供的数据库驱动类时,可能会受到限制。这是因为启动类加载器无法直接访问应用程序类加载器加载的路径。
  • SPI(Service Provider Interface)解决问题: 为了解决这个问题,Java 引入了 SPI 机制,允许应用程序通过在类路径中提供特定的配置文件,指定需要加载的服务提供者。这种机制使得应用程序可以通过自己的类加载器加载特定服务的提供者,而不受双亲委派模型的限制。
  • 当我们使用 DriverManager.getConnection()时,得到的一定是厂商实现的类。但 BootStrap ClassLoader 不能加载到各个厂商实现的类,DriverManager 的解决方案就是,在 DriverManager 初始化的时候,得到「线程上下文加载器」,去获取 Connection 的时候,是使用「线程上下文加载器」去加载 Connection 的,而这里的线程上下文加载器实际上还是 App ClassLoader,所以在获取 Connection 的时候,还是先找 Ext ClassLoader 和 BootStrap ClassLoader,只不过这俩加载器肯定是加载不到的,最终会由 App ClassLoader 进行加载
  • 那这种情况,有的人觉得破坏了双亲委派机制,因为本来明明应该是由 BootStrap ClassLoader 进行加载的,结果你来了一手「线程上下文加载器」,改掉了「类加载器」
  • 有的人觉得没破坏双亲委派机制,只是改成由「线程上下文加载器」进行类加载,但还是遵守着:「依次往上找父类加载器进行加载,都找不到时才由自身加载」。认为”原则”上是没变的。

JDBC 代码分析

引入线程上下文件类加载器(Thread Context ClassLoader)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。

JDK 使用 ServiceLoad 这个类,通过线程上下文类加载来完成加载,

  • 代码分析如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 在 JDBC 规范中明确要求 Drive 类必须向 DriverManager 注册自己,所以是用 Class.forName 也好,设置 System.setProperty 也好,都是要过 DriverManager。
    Connection con = DriverManager.getConnection(url , username , password ) ;

    public class DriverManager {
    static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
    }
    }
  • 在类初始化执行 clinit 方法的时候,会执行对应的静态代码块,也可以说这是一个类最先被执行的代码,里面有个 loadInitialDrivers 方法

    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
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    private static void loadInitialDrivers() {
    String drivers;
    //一上来先看看有没有通过 System.setProperty 配置实现类
    try {
    drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
    public String run() {
    return System.getProperty("jdbc.drivers");
    }
    });
    } catch (Exception ex) {
    drivers = null;
    }
    // If the driver is packaged as a Service Provider, load it.
    // Get all the drivers through the classloader
    // exposed as a java.sql.Driver.class service.
    // ServiceLoader.load() replaces the sun.misc.Providers()

    //这里就是另一种用 ServiceLoader 扫描的方式加载实现类,下面展开说
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
    public Void run() {

    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();

    /* Load these drivers, so that they can be instantiated.
    * It may be the case that the driver class may not be there
    * i.e. there may be a packaged driver with the service class
    * as implementation of java.sql.Driver but the actual class
    * may be missing. In that case a java.util.ServiceConfigurationError
    * will be thrown at runtime by the VM trying to locate
    * and load the service.
    *
    * Adding a try catch block to catch those runtime errors
    * if driver not available in classpath but it's
    * packaged as service and that service is there in classpath.
    */
    try{
    while(driversIterator.hasNext()) {
    driversIterator.next();
    }
    } catch(Throwable t) {
    // Do nothing
    }
    return null;
    }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
    return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
    try {
    //drivers 都扫描到了,然后就挨个加载类呗,这里用的是 Class.forName 的方式,使用 SystemClassLoader,默认是系统类加载器
    println("DriverManager.Initialize: loading " + aDriver);
    Class.forName(aDriver, true,
    ClassLoader.getSystemClassLoader());
    } catch (Exception ex) {
    println("DriverManager.Initialize: load failed: " + ex);
    }
    }
    }
  • 上面代码使用 ServiceLoader 的部分实际上是完整的加载流程,如果这段被执行,说明 drivers 为空直接 return。然后这部分代码展开细说一下。

    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
    public static <S> ServiceLoader<S> load(Class<S> service) {
    // 通过线程上下文类加载来完成加载
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    // 然后构造了一个 ServiceLoader
    return ServiceLoader.load(service, cl);
    }

    public static <S> ServiceLoader<S> load(Class<S> service,
    ClassLoader loader)
    {
    return new ServiceLoader<>(service, loader);
    }

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
    }

    public void reload() {
    providers.clear();
    // LazyIterator 参数一个 spi 接口 Driver.class,一个加载器
    lookupIterator = new LazyIterator(service, loader);
    }
  • LazyIterator 是重写了 Iterator 的 hasNext 和 next 方法

    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
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    private class LazyIterator
    implements Iterator<S>
    {

    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

    private LazyIterator(Class<S> service, ClassLoader loader) {
    this.service = service;
    this.loader = loader;
    }

    private boolean hasNextService() {
    if (nextName != null) {
    return true;
    }
    if (configs == null) {
    try {
    // "META-INF/services/java.sql.Driver"
    String fullName = PREFIX + service.getName();
    //扫描所有的 jar 包,去找 META-INF/services/java.sql.Driver 文件中获取具体的实现类名
    if (loader == null)
    configs = ClassLoader.getSystemResources(fullName);
    else
    configs = loader.getResources(fullName);
    } catch (IOException x) {
    fail(service, "Error locating configuration files", x);
    }
    }
    while ((pending == null) || !pending.hasNext()) {
    if (!configs.hasMoreElements()) {
    return false;
    }
    pending = parse(service, configs.nextElement());
    }
    // 具体的实现类名
    nextName = pending.next();
    return true;
    }

    private S nextService() {
    if (!hasNextService())
    throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
    // cn 就是数据库厂商提供的类名类名
    c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
    fail(service,
    "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
    fail(service,
    "Provider " + cn + " not a subtype");
    }
    try {
    S p = service.cast(c.newInstance());
    providers.put(cn, p);
    return p;
    } catch (Throwable x) {
    fail(service,
    "Provider " + cn + " could not be instantiated",
    x);
    }
    throw new Error(); // This cannot happen
    }

    public boolean hasNext() {
    if (acc == null) {
    return hasNextService();
    } else {
    PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
    public Boolean run() { return hasNextService(); }
    };
    return AccessController.doPrivileged(action, acc);
    }
    }

    public S next() {
    if (acc == null) {
    return nextService();
    } else {
    PrivilegedAction<S> action = new PrivilegedAction<S>() {
    public S run() { return nextService(); }
    };
    return AccessController.doPrivileged(action, acc);
    }
    }

    public void remove() {
    throw new UnsupportedOperationException();
    }

    }
  • 回到具体的查找过程

    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
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();


    try{
    while(driversIterator.hasNext()) {
    // 本质是执行的是 lookupIterator.next
    // LazyIterator 参数一个 spi 接口 Driver.class,一个加载器
    // lookupIterator = new LazyIterator(service, loader);
    driversIterator.next();
    }
    } catch(Throwable t) {
    // Do nothing
    }


    // loadedDrivers.iterator();
    public Iterator<S> iterator() {
    return new Iterator<S>() {

    Iterator<Map.Entry<String,S>> knownProviders
    = providers.entrySet().iterator();

    public boolean hasNext() {
    if (knownProviders.hasNext())
    return true;
    return lookupIterator.hasNext();
    }

    public S next() {
    if (knownProviders.hasNext())
    return knownProviders.next().getValue();
    return lookupIterator.next();
    }

    public void remove() {
    throw new UnsupportedOperationException();
    }

    };
    }

参考

https://github.com/SingleYam/overview_java

https://blog.csdn.net/weixin_43213517/article/details/89887272