JVM连接模型

驱动Java连接模型的引擎是解析【Resolution】过程。

1. 动态连接和解析

编译Java程序时,程序中的每个类或接口都会生成一个独立的class文件。尽管各个class文件可能看起来毫无关系,但它们实际上彼此之间以及与Java API的类文件之间都有符号连接。当运行程序时,Java虚拟机加载程序的类和接口,并且在动态连接的过程中将它们连接在一起。

class文件将其所有符号引用保存在一个地方,即常量池中。每个类文件都有一个常量池【constant pool】,由Java虚拟机加载的每个类或接口都有其内部版本的常量池:称为运行时常量池【runtime constant pool】。运行时常量池【runtime constant pool】是特定于实现的数据结构,它映射到class文件中的常量池【constant pool】。因此,在类型被初始加载后,该类型的所有符号引用都将驻留在该类型的运行时常量池中。

在程序运行的某个时刻,如果要使用某个特定的符号引用,则必须解析它。

解析过程指的是根据符号引用查找对应的实体,并用直接引用替换符号引用。因为所有的符号引用都驻留在常量池中,所以这个过程通常称为常量池解析。

常量池【constant pool】被组织为项【item】的序列。每一个项【item】都有一个唯一的索引,与数组类似。符号引用是可能出现在常量池中的一种项【item】。使用符号引用的Java虚拟机指令通过指定该符号引用在常量池中的索引来进行使用,如:

	getstatic:该操作码将静态字段的值压入操作数栈,
               在字节码流中,其后会跟有一个常量池索引
               而在常量池中,该索引处的项【item】是一个CONSTANT_Fieldref_info条目,
               它显示了字段所在的类的全限定名以及字段的名称和类型

来自相同或不同方法的操作指令可能均引用相同的常量池条目,但每个常量池条目仅解析一次。当符号引用被一条指令解析之后,来自其他访问该符号引用的指令均不会再解析符号引用了。

2. 解析和动态扩展

除了在运行时简单地连接类型之外,Java应用程序还可以在运行时决定连接哪个类型。

动态扩展的两种方式:

  • Class.forName(String name, boolean initialize,ClassLoader loader)
  • ClassLoader的loadClass(String name, boolean resolve)
forName

参数:

  1. name:待加载的类的全限定类名
  2. initialize:表示是否需要初始化该类,即加载-连接-初始化的初始化阶段,如果为true,那么在方法返回前,会做好加载-连接-初始化,而如果为false,那么会加载,可能会连接,但不会初始化
  3. loader:使用的类装载器

初始化是很重要的,比如JDBC驱动程序通常使用forName()方法调用装载的。

Class.forName("com.mysql.cj.jdbc.Driver");
loadClass

参数:

  1. name:全限定类名
  2. resolve:是否需要进行解析,即连接阶段的解析步骤

3.类加载器和双亲委派

在Java术语中,如果一个类装载器被要求装载一个类型,但是却返回一个由其他类装载器所装载的类型,那么这个类装载器就称为该类型的初始类装载器。实际定义该类型的类装载器称为该类型的定义类装载器

设Java应用创建了一个自定义类加载器“Customer”,并委托其加载类"java.io.FileReader",根据双亲委派,“Customer”类加载器委托给AppClassLoader,而AppClassLoader又委托给ExtClassLoader,ExtClassLoader委托给启动类加载器(即Bootstrap Class Loader),而动类加载器可以加载该类型。那么我们称动类加载器为"java.io.FileReader"类型的定义类加载器,而启动类加载器、ExtClassLoader、AppClassLoader以及“Customer”类加载器亦均称为初始类加载器。

任何被要求装载类型,并且能够返回Class实例的引用,则表示该类加载器是被装载类型的初始类装载器!!!

4.常量池解析

4.1 CONSTANT_Class_info 条目解析

这种类型的条目用于表示对类(包括数组类)或接口的符号引用。一些指令直接引用CONSTANT_Class_info条目:

  • new
  • anewarray

有一些指令通过其他类型的常量池条目,间接的引用CONSTANT_Class_info条目:

  • putfield:通过引用CONSTANT_Fieldref_info,该常量池条目的class_index项给出了CONSTANT_Class_info条目的常量池索引
  • invokevirtual

注:
当前类加载器 = 定义类加载器
当前命名空间=当前类加载器的命名空间=定义类类加载器的命名空间

数组类

如果CONSTANT_Class_info 条目的name_index 项指向的是一个以"["开头的字符串的CONSTANT_Utf8_info,那么该CONSTANT_Class_info 指向的是一个数组类。

指向数组类的符号引用的解析的最终产物是一个表示该数组类的Class实例。

(1) 如果当前类加载器 已经被记录为该数组类的初始装载器,则复用已加载生成的Class实例。
(2) 否则,那么执行如下两步:

  1. 如果数组的组件类型(组件类型指的是数组的元素类型)是引用类型,虚拟机需要使用当前类加载器 解析该组件类型,
  2. Java虚拟机使用指定的组件类型(更准确说,应该是元素类型)和维数创建一个新的数组类,并实例化一个Class实例来代表该类型。

对于引用类型的数组,该数组类的定义类加载器被标记为元素类型的定义类加载器;对于基本类型数组,数组类的定义类加载器被标记为启动类加载器

非数组类和接口

如果CONSTANT_Class_info 条目的name_index 项指向的不是一个以"["开头的字符串的CONSTANT_Utf8_info,那么该CONSTANT_Class_info 是一个指向非数组类或接口的符号引用。

Java通过如下步骤①(包括1a和1b),来解析任何指向非数组类或接口的符号引用:

步骤①

(1a)类型被加载:解析非数组类或接口所需的基本活动是确保将类型加载到当前命名空间中。作为第一步,虚拟机必须确定引用的类型是否已经加载到当前名称空间中。要进行此判断,虚拟机必须查明当前类加载器是否已被标记为该具有给定全限定名的类型的初始装载器(正在被解析的符号引用中给出的该类型的全限定名)。

  • 如果虚拟机发现该由给定的全限定名指定的类型,已经在当前命名空间中枚举出来了,它将只使用已经装载的类型(不会再触发对该类的装载),该类型由方法区中的类型数据块定义,并由堆上相关联Class实例表示。
  • 否则,虚拟机将该类型的全限定名传递给当前类装载器。Java虚拟机总是请求当前类装载器(其运行时常量池中包含正在解析的CONSTANT_Class_info条目的引用类型的定义装入器)来试图装载该类型。一旦被引用的类被装载,虚拟机会仔细检查它的二进制数据,如果该类型是一个类(排除java.lang.Object),虚拟机从类的数据中确定类的直接超类的全限定名。然后,虚拟机检查超类是否已加载到当前命名空间。如果没有,则装载超类。一旦这个类被装载完成,虚拟机就可以再次查看它的二进制数据来找到它的直接超类。这个过程一直递归到遇到java.lang.Object为止。在从Object返回的过程中,虚拟机将再次查看它加载的每个类型的类型数据,以查看该类型是否直接实现了任何接口。如果是,它将确保这些接口也被加载。对于虚拟机加载的每个接口,虚拟机亦将检查该接口的类型数据,以确定是否直接扩展了任何其他接口。如果是,虚拟机将确保加载这些超接口也被装载了。 一旦一个类型被装载进当前命名空间,且通过了递归,即该类型的所有超类和超接口也被成功加载,这时候虚拟机会为该类型创建一个Class实例来代表这个类型。

注意:对于每个类装载器,Java虚拟机维护一个列表,该列表中枚举出了由该类装载器作为初始类装载器的所有类型的名称。这些列表构成了Java虚拟机中的命名空间。虚拟机在解析【Resolution】阶段使用这些列表来确定某个类是否已经由特定的类装载器装载了。

:ClassLoader中,loadClass会调用findClass(),该方法中交给自定义类装载器来覆盖实现,如果我们可以找到或者产生一个字节数组,那么将其传递给defineClass(byte[])方法,该方法负责解析二进制数据,并将其变为方法区中的内部数据解构。

注意:通过步骤1a,Java虚拟机确保了类型被装载,如果该类型是类,那么他的所有超类和超接口也被装载。在此步骤中,这些类型没有被连接【Linking】和初始化【Initialization】,只是被装载/加载而已。
(1b)检查类型的访问权限:加载完成后,虚拟机检查访问权限。如果引用类型没有访问被引用类型的权限,则虚拟机抛出一个IllegalAccessError。步骤1b是逻辑上属于验证【Linking-Verification】的范畴,但它是在正式验证阶段之外的其他时间执行的。访问权限的检查总是在步骤1a之后进行,以确保从符号引用指向的类型均被加载到当前命名空间,这是解析该符号引用的一部分。一旦1b结束,标识着对CONSTANT_Class_info的解析也结束了。

步骤①正常结束后,由正在被解析的CONSTANT_Class_info常量池条目所引用的类型已经被成功加载【Loading】,但是还没进行必要的连接【Linking】和初始化【Initialization】。除此之外,该类型的所有超类和超接口亦都已加载,但不一定被连接【Linking】或初始化【Initialization】了(比如某个超类还作为其他类的超类,已经在其他时刻被初始化了)。

注意:超类必须在子类之前初始化
注意,只有超类必须被初始化,而不是超接口

步骤②

(2a)验证【Verify 】:验证过程可能需要虚拟机加载新的类型,以确保字节码符合Java语言的语义。例如,如果将对A类型的实例的引用分配给声明为B类型的变量,则虚拟机必须加载A和B两种类型,以确保A是B的子类,此时将加载【Loading】并连接【Linking】这些类,但肯定不会初始化【Initialization】它们。
(2b)准备【Prepare 】:在准备【Prepare 】阶段,虚拟机为类变量和特定于虚拟机实现的某些特殊数据结构(如方法表)分配内存。
(2c)解析【Resolve 】,可选的,即使虚拟机实现了提前解析【early resolution】,它必须在这个符号引用被实际使用时才会报告错误。
(2d)初始化【Initialization】:此时,类型已经加载、验证、准备和可选解析。最后,类型终于可以进行初始化了。初始化根据类与接口区分,见《类型的生命周期》第四节

4.2 CONSTANT_Fieldref_info 条目解析

要解析类型为CONSTANT_Fieldref_info的常量池条目,虚拟机必须首先解析其class_index项中指定的CONSTANT_Class_info条目。

如果对CONSTANT_Class_info的解析成功完成,则虚拟机使用以下步骤执行字段查找过程:

  1. 虚拟机在被引用的类型中查找具有指定名称和类型的字段。如果虚拟机发现这样一个字段,则该字段是成功查找到的结果字段。
  2. 否则,虚拟机检查该类型直接实现或扩展的接口,以及递归的检查他们的超接口。如果虚拟机找到这样一个字段,则该字段是成功查找到的结果字段。
  3. 否则,如果该类型有直接超类,虚拟机检查其直接超类,并递归的检查所有的超类。如果虚拟机找到这样一个字段,则该字段是成功查找到的结果字段。
  4. 字段搜索失败

如果搜索成功,虚拟机将该条目标记为已解析,并在该条目的数据中放上指向这个字段的引用。

4.3 CONSTANT_Methodref_info 条目解析

要解析类型为CONSTANT_Methodref_info 的常量池条目,虚拟机必须首先解析其class_index项中指定的CONSTANT_Class_info条目。

如果对CONSTANT_Class_info的解析成功完成,则虚拟机使用以下步骤执行方法查找过程:

  1. 如果被解析的类型是接口,抛出IncompatibleClassChangeError
  2. 否则,虚拟机检查被引用的类是否具有指定名称和描述符的方法。如果虚拟机发现了这样的方法,那么该方法就是成功查找到的结果方法。
  3. 否则,如果该类存在直接超类,虚拟机检查其直接超类,并递归检查其所有超类。如果虚拟机发现了这样的方法,那么该方法就是成功查找到的结果方法。
  4. 否则,虚拟机检查该类直接实现的所有超接口。如果虚拟机发现了这样的方法,那么该方法就是成功查找到的结果方法。
  5. 方法搜索失败

如果搜索成功,虚拟机将该条目标记为已解析,并在该条目的数据中放上指向这个方法的引用。

4.4 CONSTANT_InterfaceMethodref_info 条目解析

要解析类型为CONSTANT_InterfaceMethodref_info的常量池条目,虚拟机必须首先解析class_index项中指定的CONSTANT_Class_info条目。

如果对CONSTANT_Class_info的解析成功完成,则虚拟机使用以下步骤执行方法查找过程:

  1. 如果被解析的类型是类,而不是接口,则虚拟机将抛出IncompatibleClassChangeError。
  2. 否则,被解析的类型是接口。虚拟机检查被引用的接口,查找具有指定名称和描述符的方法。如果虚拟机发现了这样一个方法,那么该方法就是成功查找到的结果接口方法。
  3. 否则,虚拟机检查直接接口,并递归检查该接口的所有超接口。如果虚拟机发现了这样一个方法,那么该方法就是成功查找到的结果接口方法。
  4. 接口方法搜索失败

如果搜索成功,虚拟机将该条目标记为已解析,并在该条目的数据中放上指向这个接口方法的引用。

4.5 CONSTANT_String_info 条目解析

为了解析CONSTANT_String_info条目,虚拟机必须在要解析的CONSTANT_String_info 常量池条目的数据中放置对拘禁的【interned 】字符串对象的引用。该String对象(类java.lang.String的实例)必须按照由CONSTANT_String_info条目的string_index项指定的字符顺序进行组织。

每个Java虚拟机必须维护一个对字符串对象引用的内部列表【internal list 】,这些对象在运行应用程序期间被“拘禁【interned】”。基本上,如果字符串对象出现在虚拟机的字符串拘禁列表中,那么就说它是被“拘禁【interned】”的。

维护这个内部列表的目的是保证任何特定的字符序列在该列表中仅出现一次。

为了拘禁CONSTANT_String_info条目所代表的字符序列,虚拟机的执行逻辑如下:

  1. 检查拘禁内部列表中,该字符串是否已经在编,如果在编,虚拟机直接是用指向以前拘留的字符串对象的引用
  2. 否则,虚拟机按照字符序列创建一个新的字符串对象,并将这个对象记录到拘禁列表中

为了完成对CONSTANT_String_info条目的解析过程,虚拟机将对被拘禁的字符串对象的引用放置到正在被解析的CONSTANT_String_info常量池条目的数据中。

拓展:在Java程序中,可以通过调用String类的intern()方法,来拘禁该字符串。所有字符串字面量(字符串字面量指的是源码中可见的字符串值),在解析CONSTANT_String_info时候被拘禁。而如果一个具有相同Unicode字符序列的字符串已经被拘禁过,那么intern()返回的是对所匹配的已拘禁的字符串对象的引用。

4.6 其他类型的常量池条目的解析

CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info 这些条目本身包含他们所表示的常量值,可以直接被解析。

不需要解析,直接用就可以了。

CONSTANT_Utf8_info 、CONSTANT_NameAndType_info 永远不会被字节码指令引用,他们只会被其他的常量池条目引用,并在引用它的条目解析时,它们才会被解析。

5.编译时常量解析

对被初始化为编译时常量的static final变量的引用,在编译时被解析为常量值的本地副本拷贝。对于所有基本类型和String类型均有效

常量的这种特殊处理为Java语言的两个特性提供了便利:

  • 条件编译。
  • static final的变量可以用于switch的case表达式,虚拟机使用tableswitch(效率高)和lookupswitch两种操作码来实现switch,均需要case值内嵌在字节码流中。

6.直接引用

常量池解析的最终目标是将符号引用替换为直接引用

指向类型【types】、类变量【class variables】和类方法【class methods 】的直接引用,很可能是指向方法区的本地指针【native pointers】。

  • 对类型的直接引用可能简单地指向保存类型数据的方法区中特定于实现的数据结构。
  • 对类变量的直接引用可能指向存储在方法区中的类变量的值。
  • 对类方法的直接引用可能指向方法区中的一个数据结构,该结构包含调用方法所需的数据。例如,类方法的数据结构可以包含诸如方法是否是native的等信息。如果该方法是native的,则数据结构可以包括一个指向动态链接的本机方法实现的函数指针。如果该方法不是native的,那么该数据结构可以包括方法的字节码、max_stack、max_local值等等。

对实例变量和实例方法的直接引用一般是偏移量

  • 对实例变量的直接引用很可能是从对象的内存映像开始算起到这个实例变量位置的偏移量。
  • 对实例方法的直接引用很可能是方法表中的偏移量。

你可能感兴趣的:(JVM)