java虚拟机学习笔记——连接模型(第八章)

来源:互联网 发布:android 文件存储数据 编辑:程序博客网 时间:2024/06/05 19:00

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类型的参数表示是否在装载时执行该类型的连接。

区别:如果没有特别要使用类装载器的要求,应该用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:初始化类型

        如果类型拥有超类,初始化类型的超类是按自顶向下的顺序进行的。

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.8解析CONSTANT_String_info入口

     每一个java虚拟机必须维护一张内部列表,它列出了所有在运行程序的过程中已被“拘留(intern)”的字符串对象的引用。基本上,如果一个字符串在虚拟机的拘留列表上出现,就说它被拘留了。维护这个列表的关键是任何特定的字符序列在这个列表上都只出现一次。

     要拘留CONSTANT_String_info入口所代表的字符序列,虚拟机要检查内部拘留名单上这个字符序列是否已经在编了。如果已经在编,虚拟机使用指向以前拘留的字符串对象的引用。否则虚拟机按照这个字符序列创建一个新的字符对象,并把这个对象的引用编入列表。

     在Java程序中,可以调用String类的intern()方法来拘留一个字符串。

8.1.11编译时常量解析

     被初始化为编译时常量的静态final变量的引用,在编译时被解析为常量值的一个本地拷贝,这对所有基本类型和java.lang.String都是正确的。

有两个好处:1)常量值的本地拷贝使得静态final变量可以用于switch语句中的case表达式。2)条件编译。


原创粉丝点击