驱动Java连接模型的引擎是解析【Resolution】过程。
编译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条目,
它显示了字段所在的类的全限定名以及字段的名称和类型
来自相同或不同方法的操作指令可能均引用相同的常量池条目,但每个常量池条目仅解析一次。当符号引用被一条指令解析之后,来自其他访问该符号引用的指令均不会再解析符号引用了。
除了在运行时简单地连接类型之外,Java应用程序还可以在运行时决定连接哪个类型。
动态扩展的两种方式:
参数:
初始化是很重要的,比如JDBC驱动程序通常使用forName()方法调用装载的。
Class.forName("com.mysql.cj.jdbc.Driver");
参数:
在Java术语中,如果一个类装载器被要求装载一个类型,但是却返回一个由其他类装载器所装载的类型,那么这个类装载器就称为该类型的初始类装载器。实际定义该类型的类装载器称为该类型的定义类装载器。
设Java应用创建了一个自定义类加载器“Customer”,并委托其加载类"java.io.FileReader",根据双亲委派,“Customer”类加载器委托给AppClassLoader,而AppClassLoader又委托给ExtClassLoader,ExtClassLoader委托给启动类加载器(即Bootstrap Class Loader),而动类加载器可以加载该类型。那么我们称动类加载器为"java.io.FileReader"类型的定义类加载器,而启动类加载器、ExtClassLoader、AppClassLoader以及“Customer”类加载器亦均称为初始类加载器。
任何被要求装载类型,并且能够返回Class实例的引用,则表示该类加载器是被装载类型的初始类装载器!!!
这种类型的条目用于表示对类(包括数组类)或接口的符号引用。一些指令直接引用CONSTANT_Class_info条目:
有一些指令通过其他类型的常量池条目,间接的引用CONSTANT_Class_info条目:
注:
当前类加载器 = 定义类加载器
当前命名空间=当前类加载器的命名空间=定义类类加载器的命名空间
如果CONSTANT_Class_info 条目的name_index 项指向的是一个以"["开头的字符串的CONSTANT_Utf8_info,那么该CONSTANT_Class_info 指向的是一个数组类。
指向数组类的符号引用的解析的最终产物是一个表示该数组类的Class实例。
(1) 如果当前类加载器 已经被记录为该数组类的初始装载器,则复用已加载生成的Class实例。
(2) 否则,那么执行如下两步:
对于引用类型的数组,该数组类的定义类加载器被标记为元素类型的定义类加载器;对于基本类型数组,数组类的定义类加载器被标记为启动类加载器。
如果CONSTANT_Class_info 条目的name_index 项指向的不是一个以"["开头的字符串的CONSTANT_Utf8_info,那么该CONSTANT_Class_info 是一个指向非数组类或接口的符号引用。
Java通过如下步骤①(包括1a和1b),来解析任何指向非数组类或接口的符号引用:
(1a)类型被加载:解析非数组类或接口所需的基本活动是确保将类型加载到当前命名空间中。作为第一步,虚拟机必须确定引用的类型是否已经加载到当前名称空间中。要进行此判断,虚拟机必须查明当前类加载器是否已被标记为该具有给定全限定名的类型的初始装载器(正在被解析的符号引用中给出的该类型的全限定名)。
注意:对于每个类装载器,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】:此时,类型已经加载、验证、准备和可选解析。最后,类型终于可以进行初始化了。初始化根据类与接口区分,见《类型的生命周期》第四节
要解析类型为CONSTANT_Fieldref_info的常量池条目,虚拟机必须首先解析其class_index项中指定的CONSTANT_Class_info条目。
如果对CONSTANT_Class_info的解析成功完成,则虚拟机使用以下步骤执行字段查找过程:
如果搜索成功,虚拟机将该条目标记为已解析,并在该条目的数据中放上指向这个字段的引用。
要解析类型为CONSTANT_Methodref_info 的常量池条目,虚拟机必须首先解析其class_index项中指定的CONSTANT_Class_info条目。
如果对CONSTANT_Class_info的解析成功完成,则虚拟机使用以下步骤执行方法查找过程:
如果搜索成功,虚拟机将该条目标记为已解析,并在该条目的数据中放上指向这个方法的引用。
要解析类型为CONSTANT_InterfaceMethodref_info的常量池条目,虚拟机必须首先解析class_index项中指定的CONSTANT_Class_info条目。
如果对CONSTANT_Class_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条目所代表的字符序列,虚拟机的执行逻辑如下:
为了完成对CONSTANT_String_info条目的解析过程,虚拟机将对被拘禁的字符串对象的引用放置到正在被解析的CONSTANT_String_info常量池条目的数据中。
拓展:在Java程序中,可以通过调用String类的intern()方法,来拘禁该字符串。所有字符串字面量(字符串字面量指的是源码中可见的字符串值),在解析CONSTANT_String_info时候被拘禁。而如果一个具有相同Unicode字符序列的字符串已经被拘禁过,那么intern()返回的是对所匹配的已拘禁的字符串对象的引用。
CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info 这些条目本身包含他们所表示的常量值,可以直接被解析。
不需要解析,直接用就可以了。
CONSTANT_Utf8_info 、CONSTANT_NameAndType_info 永远不会被字节码指令引用,他们只会被其他的常量池条目引用,并在引用它的条目解析时,它们才会被解析。
对被初始化为编译时常量的static final变量的引用,在编译时被解析为常量值的本地副本拷贝。对于所有基本类型和String类型均有效。
常量的这种特殊处理为Java语言的两个特性提供了便利:
常量池解析的最终目标是将符号引用替换为直接引用。
指向类型【types】、类变量【class variables】和类方法【class methods 】的直接引用,很可能是指向方法区的本地指针【native pointers】。
对实例变量和实例方法的直接引用一般是偏移量。