8.1 动态连接和解析
Class文件把它所有的引用符号保存在一个地方——常量池。每一个class文件有一个常量池,每一个被Java虚拟机装载的类或者接口都有一份内部版本的常量池,被称作运行时常量池。运行时常量池是一个特定于实现的数据结构,数据结构映射到class文件中的常量池。因此当一个类型被首次装载时,所有来自于类型的符号引用都装载到了类型的运行时常量池。
当程序运行到某个时刻,如果某个特定的符号引用将要被使用,它首先要被解析。解析过程就是根据符号引用查找到实体,再把符号引用替换成一个直接引用的过程。因为所有的符号引用都保存在常量池中,所以这个过程常被称作常量池解析。
来自相同或不同方法中的几条指令,可能指向同一个常量池入口,但是每一个常量池入口都只被解析一次。当符号引用被一条指令解析过后,来自其他指令的访问该符号引用的后续尝试认为这项工作已经完成都使用第一次解析出的直接引用结果。
记住:java虚拟机为每一个装载的类和接口保存一份独立的常量池。
8.1.1解析和动态扩展
Java的体系结构允许动态扩展Java程序,这个过程包括运行时决定所使用的类型,装载它们,使用它们。通过传递类型的名字到java.lang.Class的forName()方法,或者用户自定义的类装载器的loadClass()方法,可以动态扩展Java程序。动态扩展的两种方式:
1) 直接使用java.lang.Class的forName()方法
Public static Class forName(String className)throwsClassNotFoundException;
//该方法使用当前类的类装载器,并且总是初始化该类型
Public satic Class forName(String className,Booleaninitialize,ClassLoader loader)throws ClassNotFoundException;
//initialize参数可以指定是否在装载完后进行初始化,loader可以指定装载的类装载器。
2) 使用用户自定义的类装载器的loadClass()方法
如果需要用自定义的类装载器请求类型,只需要调用那个类装载器的loadClass()方法。
Protected Class loadClass(String name)throws ClassNotFoundException
Protected Class loadClass(String name,Boolean resolve)throwsClassNotFoundException;
两个loadClass()方法都接受装载类型的全限定名装入String类型的name参数。双参数版本的loadClass()中,boolean类型的参数表示是否在装载时执行该类型的连接。
初始化是很重要的,比如JDBC驱动程序常用forName()调用装载的,因为每一个JDBC驱动程序类的静态初始化方法都用DriverManager注册驱动程序,这样才能被应用程序所使用。驱动程序类必须被初始化,而不是仅仅被加载。
区别:如果没有特别要使用类装载器的要求,应该用forName(),如果需要请求的类型在装载时就初始化的话,则不得不使用forName();如果需要一些特定的装载类型的方法,比如从网络上下载,从数据库中取出,从加密文件中提取,甚至动态地创建它们,这时就需要一个类装载器。
8.1.4 解析CONSTANT_Class_info入口
常量池入口类型中,解析起来最复杂的就是CONSTANT_Class_info了。
这种入口类型用来表示指向类(包括数组类)和接口的符号引用。
数组类的解析:
每一个数组在虚拟机中都会被解析成一人Class实例,如果数组的元素类型是一个引用类型,虚拟机用当前类装载器解析元素类型。如果数组的元素类型是一个基本类型,那么虚拟机立即创建关于那个元素类型的新数组类,维数也在此时确定,然后创建一个Class的实例来代表这个类型。如果是关于引用的数组,数组会标记为是由定义它的元素类型的类装载器定义的。如果是关于基本类型的数组,数组类会被标记为是由启动类装载器定义的。
非数组类和接口的解析:
由于需要分多步来解析,下面以1a到2d来说明这一过程。
步骤1:作为解析的第一步,虚拟机必须确定是否被引用的类型已经被装载进了当前命名空间,如果没有被装载进当前命名空间,虚拟机把类型的全限定名传递给当前类装载器。
步骤1a:装载类型或者任何超类型
第一步,虚拟机必须确定是否被引用的类型已经被装载进了当前命名空间。为了作出决定,虚拟机必须查明是否当前类装载器被标记为该类型的初始化装载器
对于每一个类装载器,Java虚拟机维护一张列表,其中记录了所有其装载的类型的名字。每一张这样的列表就组成了java虚拟机内部的命名空间。在解析过程中,虚拟机使用这些列表来决定是否一个类已经被一个特定的类装载器装载过了,虚拟机会使用双亲委派模型来装载类型,一旦被引用的类型被装载了,虚拟机仔细检查它的二进制数据。如果类型是一个类,并且不是java.lang.Object,虚拟机根据类的数据得到它的直接超类的全限定名。虚拟机接着会察看超类是否已经被装载进当前命名空间了。如果没有,先装载超类。一旦超类被装载了,虚拟机再次检查它的二进制数据来找到它的超类。一直重复到超类为Object为止。然后在从Object返回的路上,虚拟机再次检查每个类型是否直接实现了任何接口,如果这样,它会确保那些接口也被装载了。经过步骤1a,java虚拟机确认某个类型是否被装载了,并确保它的所有超类和所有超接口都被装载了。
步骤1b:检查访问权限
如果发起引用的类型没有访问被引用的类型的权限,虚拟机抛出IllegalAccessError异常。检查访问权限是在正式校验阶段之前进行的。
步骤2:连接并初始化类型和任何超类
如果一个类型还没有被连接,在初始化之前必须被连接,注意只有超类必须被初始化,超接口是不必的。
步骤2a:校验类型
这一步就是第七章中的正式校验阶段,校验阶段可能要求虚拟机装载新的类型来确认字节码符合java语言的语义。
步骤2b:准备类型
在准备阶段虚拟机为类型变量以及随实现不同而有差别的数据结构(如方法表)分配内存。
步骤2c:可选的步骤,解析类型
步骤1a、2a、2b已经解析了发起引用的类型的常量池的CONSANT_Class_info入口。步骤2c是关于被引用类型中所包含的符号引用的解析。
步骤2d:初始化类型
初始化包括两个步骤,如果类型拥有任何超类,初始化类型的超类是按照自顶向下的顺序进行的,如果类型有一个类初始化方法,那也在此时执行。如果类拥有类初始化方法,恰好是在步骤2d执行它
8.1.5解析CONSTANT_Fieldref_info入口
要解析类型是CONSTANT_Fieldref_info的常量池入口,虚拟机必须首先解析class_index项中指明的CONSTANT_Class_info入口。如果CONSTANT_Class_info解析成功,虚拟机在此类型和它的超类型上搜索所需要的字段,如果找到需要的字段,虚拟机要检查当前类是否拥有访问这个字段的权限,虚拟机按照如下步骤执行字段搜索过程:
1) 虚拟机在被引用的类型中查找具有指定的名字和类型的字段。如果虚拟机找到了这样一个字段,这个字段就是成功的字段搜索结果。
2) 否则,虚拟机检查类型直接实现或扩展的接口,以及递归地检查它们的接口。如果找到名字和类型都符合的字段,这个字段就是成功的字段搜索结果。
3) 否则,如果类型拥有一个直接的超类,虚拟机检查类型的直接超类,并且递归地检查类型的所有超类,如果找到了名字和类型都符合的字段,这个字段就是成功的字段搜索结果。
4) 字段搜索失败。
如果字段搜索到,虚拟机把这个入口标记为已解析,并在这个常量池入口的数据中放上指向这个字段的直接引用。
8.1.6解析CONSTANT_Methodref_info入口
要解析类型是CONSTANT_Methodref_info的常量池入口,虚拟机必须首先解析class_index项中指明的CONSTANT_Class_info入口。如果CONSTANT_Class_info解析成功,虚拟机在类型和它的超类型中搜索指定的方法,如果找到了指定的方法,虚拟机检查当前类是否有权限去访问这个方法。虚拟机按照如下步骤执行方法解析:
1) 如果被解析的类型是一个接口,而非类,虚拟机抛出IncompatibleClassChangeError
2) 否则,虚拟机检查被引用的类是否有一个方法符合指定的名字以及描述符。如果虚拟机找到了这样一个方法,这个方法就是成功的方法搜索结果。
3) 否则,如果类型拥有一个直接的超类,虚拟机检查类型的直接超类,并且递归地检查类型的所有超类,查找是否有一个方法符合指定的名字以及描述符,如果找到了这样一个方法,这个方法就是成功的字段搜索结果。
4) 否则,虚拟机检查类型直接实现或扩展的接口,以及递归地检查它们的接口。查找是否有一个方法符合指定的名字以及描述符,如果找到了这样一个方法,这个方法就是成功的字段搜索结果。
5) 否则,方法搜索失败。
如果方法搜索到,虚拟机把这个入口标记为已解析,并在这个常量池入口的数据中放上指向该方法的直接引用。
8.1.7解析CONSTANT_InterfaceMethodref_info入口
要解析类型是CONSTANT_InterfaceMethodref_info的常量池入口,虚拟机必须首先解析class_index项中指明的CONSTANT_Class_info入口。如果解析CONSTANT_Class_info成功,虚拟机在接口和它的超类型中搜索指定的方法(不需要确认访问方法的权限,因为接口中定义的所有方法都是隐含公开的),虚拟机按照如下步骤执行方法解析:
(1)如果被解析的类型是一个类,而非接口,虚拟机抛出IncompatibleClassChangeError异常。
(2)否则,被解析的类型是一个接口,虚拟机检查被引用的接口是否有方法符合指定的名字和描述符,如果发现了这样的一个方法,该方法就是成功的接口方法搜索结果。
(3)否则,虚拟机检查接口的直接超接口,并且递归的检查接口的所有超接口以及java.lang.Object类来查找符合指定名字和描述符的方法,如果发现了这样的一个方法,该方法就是成功的接口方法搜索结果。
(4)如果虚拟机没有在被引用的接口和它的任何超类中找到名字,返回类型,参数的数量和类型都符合的方法,虚拟机抛出NoSuchMethodError异常
8.1.8解析CONSTANT_String_info入口
要解析类型是CONSTANT_String_info的入口,java虚拟机必须把一个指向字符串对象的引用放置到要被解析的常量池入口数据中,该字符串对象(java.lang.String类的实例)必须按照string_index项在CONSTANT_String_info中指明的CONSTANT_Utf8_info入口所指定的字符顺序组织。
每一个java虚拟机必须维护一张内部列表,它列出了所有在运行程序的过程中已被“拘留(intern)”的字符串对象的引用。基本上,如果一个字符串在虚拟机的拘留列表上出现,就说它被拘留了。维护这个列表的关键是任何特定的字符序列在这个列表上都只出现一次。
要拘留CONSTANT_String_info入口所代表的字符序列,虚拟机要检查内部拘留名单上这个字符序列是否已经在编了。如果已经在编,虚拟机使用指向以前拘留的字符串对象的引用。否则虚拟机按照这个字符序列创建一个新的字符对象,并把这个对象的引用编入列表。
在Java程序中,可以调用String类的intern()方法来拘留一个字符串。
8.1.11编译时常量解析
被初始化为编译时常量的静态final变量的引用,在编译时被解析为常量值的一个本地拷贝,这对所有基本类型和java.lang.String都是正确的。
有两个好处:1)常量值的本地拷贝使得静态final变量可以用于switch语句中的case表达式。2)条件编译。
8.1.12直接引用
常量池解析的最终目标是把符号引用替换为直接引用,指向类型,类变量和类方法的直接引用可能是指向方法区得本地指针。类型的直接引用可能简单的指向保存类型数据的方法区中的与实现相关的数据结构,类变量的直接引用可以指向方法区中保存的类变量的值。指向实例变量和实例方法的直接引用都是偏移量。使用偏移量来表示实例变量和实例方法的直接引用,取决于类的对象映像中字段的顺序和类方法表中方法的顺序的预先决定。因此不管何时java虚拟机从接口引用调用一个方法,他必须搜索对象的类的方法表来找到一个合适的方法。不管怎样,给定接口引用时调用方法总是比给定类引用时调用方法慢很多。
java.lang.ClassLoader类中的loadClass()的具体实现通过如下步骤来实现loadClass()方法的工作:
(1)查看是否请求的类型已经被这个类装载器装载进命名空间了(通过findLoadedClass())如果的确如此,返回这个已经装载的类型的Class实例。
(2)否则,委派到这个类装载器的双亲装载器,如果双亲返回了一个Class实例,就把这个Class实例返回。
(3)否则,调用findClass(),它会试图寻找或者生成一个字节数组,内容采用java class文件格式,如果成功findClass()把这个字节传递给defineClass(),后者试着导入这个类型,返回一个Class实例,如果findClass返回一个Class实例,loadClass()就把这个class实例返回。
(4)否则,findClass()抛出某些异常来终止处理,而且loadClass()也抛出同样的异常。