8.1.4 解析CONSTANT_Class_info入 口

8.1.4 解析CONSTANT_Class_info入 口

在所有常量池入口类型中,解析起来最复杂的就是CONSTANT_Class_info了。这种人口 类型用来表示指向类(包括数组类)和接口的符号引用。有几个指令,比如new和anewarray, 直接使用CONSTANT_Class_info人口。其他的指令,比如putfield或者invokevirtual,从其他类 型的人口间接指向CONSTANT_Class_info。举个例子来说,putfield指令使用CONSTANT_Fieldref_info人口。CONSTANT_Fieldref_info的class_index项目则包含指向CONSTANT_Class_info人口的常量池索引。

根据类型是否是数组,或者引用的类型(包含正在被解析的CONSTANT_Class_info人口的 常量池的类型)是由启动类装载器还是由用户自定义的类装载器装载的,解析CONSTANT_Class_info入口的细节会有所不同。

1.数组类

如果一个 CONSTANT_Class_info人 口 的 name_index 项指向的 CONSTANT_Utf8_info 字符串 是由一个左方括号开始的,比如“[I”,那么它指向的是一个数组类。在第6章中描述过,内部使用的数组名字的每一维使用一个左方括号,然后是元素的类型。如果元素类型由一个“L”开头, 比如“Ljava.lang.Integer;”,那么数组是一个关于引用的数组;否则,元素类型是一个基本类型, 比如“I”表示int, “D”表示double,数组就是一个基本类型组成的数组。

指向数组类的符号引用的最终解析结果是一个Class实例,表示该数组类。如果当前类装载 器已经被记录为被解析的数组类的初始装载器,就使用同样的类。否则,虚拟机执行下列步骤: 如果数组的元素类型是一个引用类型(数组是一个关于引用的数组),虚拟机用当前类装载器解析元素类型。举例来说,如果解析名为“[[Ljava.lang.Integer”的数组类,虚拟机会确认java.lang.Integer被装载到当前类装载器的命名空间中。如果数组是关于基本类型的数组,那么虚拟 机立即就会创建关于那个元素类型的新数组类,维数也在此时确定,然后创建一个Class的实例 来代表这个类型。如果数组是关于引用的数组,那么这一步骤发生在解析了元素类型之后。如果是关于引用的数组,数组类会被标记为是由定义它的元素类型的类装载器定义的。如果是关于 基本类型的数组,数组类会被标记为是由启动类装载器定义的。

2.非数组类和接口

如果CONSTANT_Class_info人口的name_index项指向一个并非由左方括号开始的 CONSTANT_Class_info字符串,那么这是一个指向非数组类或者接口的符号引用。解析这种类型的符号引用分为多步。

要解析任何指向非数组类或者接口的符号引用(任何CONSTANT_Class_info人口),Java虚 拟机都要执行相同的基本步骤。下面我们用步骤la和步骤lb来说明。在步骤la,类型被装载。 在步骤lb,检查访问类型的权限。虚拟机执行步骤la的精确方式取决于该引用类型是被启动类 装载器装载,还是被用户自定义的类装载器装载。

这一部分还要说明步骤2a到步骤2d,它们描述了如何连接和初始化新解析的类型。对于将 要被连接和初始化的类型来说,这些步骤并不是解析的一部分。解析非数组类或者接口只包含 步骤1a和步骤lb,(潜在的)装载类并检査它的访问权限。然而如果解析符号引用到类型是由第 一次使用这个类型触发的,在解析类型的符号引用之后,立即开始连接并初始化这个类型。因 为Java虚拟机的实现允许进行早期解析,尤论如何,解析类型的引用可能远早于连接和初始化这些类型。第7章曾提到,初始化(这是步骤2d)在第一次使用这个类型时激活。在类型可以被初 始化之前,它首先要被连接(步骤2a到步骤2c ),而在它能够被连接之前,它必须被装载(步骤 la)。

步骤1a:装载类型或者任何超类型

解析非数组类或者接口的基本要求是确认类型被装载到了当前命名空间。作为第一步,虚拟机必须确定是否被引用的类型已经被装载进了当前命名空间。为了做出决定,虚拟机必须查 明是否当前类装载器被标记为该类型的初始装载器(该类型包含所需的全限定名,是由被解析 的符号引用给出)。对于每一个类装载器,Java虚拟机维护一张列表,其中记录了所有其类装载 器是一个初始类装载器的类型的名字(解释:其中记录了类型的名字,该类装载器是这些类型的初始类装载器)。每一张这样的列表就组成了java虚拟机内部的命名空间。 在解析过程中,虚拟机使用这些列表来决定是否一个类已经被一个特定的类装载器装载过了。 如果虚拟机发现希望装载的全限定名已经在当前命名空间被列出了,它将只使用已经被装载的 类型,该类型由方法区的类型数据块所定义,并由堆中相关的Class实例所表示。首先检查当前 命名空间是否已经包含了希望装载的全限定名,虚拟机保证每一个类装载器都只装载一个给定 名宇的类型。

如果希望装载的类型还没有被装载进当前命名空间,虚拟机把类型的全限定名传递给当前 类装载器。Java虚拟机总是要求当前类装载器,就是发起引用的类型的定义类装载器,也就是运行时常量池包含正在被解析的CONSTANT_Class_info人口的类装载器,来试图装载被引用的类 型。如果发起引用的类型是被启动类装载器定义的,虚拟机要求启动类装载器调用被引用的类 型。否则,发起引用的类型是被一个用户自定义的类装载器定义的,虚拟机就要求同一个类装 载器来装载被引用的类型。

如果当前类装载器就是启动类装载器,虚拟机根据不同的实现使用不同的方式装载类型。 如果当前类装载器是一个用户自定义的类装载器,Java虚拟机通过调用用户自定义类装载器的 loadClass ()方法来完成装载请求,把需要装载的类型的全限定名作为参数传递进去。

装载类型时,不管是请求启动类装载器还是用户自定义的类装载器。都有两个选择:类装 载器可以选择自行装载这个类型,或者委派其他的类装载器完成这个工作。用户自定义的类装 载器可能要求另一个用户自定义的类装载器或者启动类装载器来试着装载这个类型;而启动类 装载器可能要求一个用户自定义的类装载器来试着装载这个类型。

若要委派给一个用户自定义的类装载器,类装载器(不管是启动类装载器或者是用户自定 义的类装载器)调用被委派的类装载器的loadClass ()方法,把需要装载的类型的全限定名作 为参数来传递。若要委派给启动类装载器,一个用户自定义类装载器调用java.lang.ClassLoader 的一个静态方法findSystemClass (),把需要装载的类型的全限定名作为参数来传递。被委派的 类装载器可以决定是否自行装载该类型,或者委派给另一个类装载器。最终,某个类装载器决定到此为止了,不需要再委派了,然后自己去装载类型。如果装载成功,那么这个类装载器就 被标记为该类型的定义类装载器。在这个过程中涉及的所有类装载器一定义类装载器和所有 产生委派的类装载器——都被标记为该类型的初始装载器。

考虑到本章前面提到的双亲委派模型的存在,如果一个用户自定义的类装载器产生委派,它 委派的往往是在双亲委派模型中的双亲。双亲再委派给它的双亲,以此类推。委派的过程一直进 行到委派的末端,有一个类装载器不再委派,而是决定装载这个类型。大多数情况下,末端的类 装载器就是启动类装载器。如果一个双亲类装载器试图装载这个类型但是却失败了,控制权重新 回到子装载器。在双亲委派模型中,子装载器在得知它的双亲(包括祖父,曾祖父……)无法装 载此类型时,它会试图自行装载。如果委派链中的某个类装载器第一个成功地装载了类型,那么这个类装载器就会被标记为定义类装载器。这个类装载器以及所有在委派链中排在它前面的类装 载器会被标记为初始类装载器。然而,它的双亲、祖父、曾祖父,以及更上一代,他们没有一个 成功装载了这个类型,所以不会被标记为类型的初始类装载器。

如果用户自定义类装载器的loadClass ()方法能够找到或者产生一个宇节数组,用Java class文件格式描述了该类型,loadClass ()必须调用defineClass (),把类型全限定名和指向那 个字节数组的引用传递进去。调用defineClass ()方法会使得虚拟机试图解析二进制数据,变为 方法区中的内部数据结构。这时候虚拟机会进行一项在第3章描述过的检查,要保证传递来的字 节数组按照Java class文件格式的基本结构组织。Java虚拟机用传递进来的全限定名来校验,需 要装载的类型名字是否就是传递迸来的字节数组定义的类型名字。

一旦被引用的类型被装载了,虚拟机仔细检査它的二进制数据。如果类型是一个类,并且不是java.lang.Object,虚拟机根据类的数据得到它的直接超类的全限定名。虚拟机接着会察看超类是否已经被装载进当前命名空间了。如果没有,先装载超类。一旦超类被装载了,虚拟机再 次仔细检査它的二进制数据来找到它的超类。一直重复到超类为Object为止。

当虚拟机调用超类的时候,它实际上只是解析另外一个符号引用。为了确定一个类的超类的全限定名,虚拟机察看class文件的superclass域。这个域给出了一个CONSTANT_Class_info 常量池人口的索引,作为指向类的超类的符号引用。但虚拟机装载超类的时候,它对超类的符号引用执行解析步骤1a。从而,作为解析CONSTANT_Class_info人口过程步骤la的一部分,虚 拟机递归地在每一个超类上应用解析CONSTANT_Class_info人口的过程,直到最终遇到Object。

在从Object返回的路上,虚拟机再次仔细检査每个类型的数据,看它们是否直接实现了任何 接口。如果是这样,它会先确保那些接口也被装载了。对于每一个虚拟机装载的接口,虚拟机 检査它们的类型数据,看它们是否直接扩展了任何其他接口。如果是这样,虚拟机会确认那些 超接口也被装载了。

当虚拟机装载超接口时,它再次解析更多眵CONSTANT_Class_info人口。正在被装载的类型直接实现或者扩展的接口保存在class文件的interfaces元素中,它实际保存的是作为符号引用 的一些常量池人口。当虚拟机装载超接口时,它解析interfaces元素中指定的CONSTANT_Class_info入口,递归地应用CONSTANT_Class_info人口的解析过程。

当虚拟机递归地在超类和超接口上应用解析过程时,它使用发起引用的子类型的定义类装载 器。虚拟机来用通常的方式做出请求,这是通过调用发起引用的子类型的定义类装载器的 loadClass ()方法实现的,把需要装载的直接超类或者直接超接口的全限定名作为参数传递进去。

—旦一个类型被装载进入了当前命名空间,而且通过递归,所有该类型的超类和超接口也 都被成功装载了,虚拟机就会创建新的Class实例来代表这个类型。如果定义类型的字节是由用 户自定义的类装载器确定或者生成,然后传递到defineClass ()方法,defineClass ()会在这个 时候返回这个新的Class实例。或者,如果用户自定义的类装载器通过findSystemClass ()调用 委派启动类装载器来装载,findSystemClass ()会在这个时候返回Class实例。直到从 defineClass ()方法或者findSystemClass ()方法接收到了Class实例,loadClass ()方法才会 返回这个Class实例给它的调用者。如果用户自定义的类装载器委派给了另一个用户自定义的类 装载器,那么,当被委派的用户自定义类装载器的loadClass ()方法返回时,它会收到Class实例。直到从被委派的类装载器收到Class实例,发起委派的类装载器才会从它自己的loadClass ()方法返回这个实例。

通过步骤la,java虚拟机确认某个类型是否被装载了,如果这个类型是一个类,确保它的所有超类都被装载了。不管这个类型是类还是接口,Java虚拟机确保它的所有超接口也都被装载了。 在这个步骤中,这些类型没有被连接或者初始化——仅仅是装载。

在步骤la,虚拟机可能抛出如下错误:

•如果虚拟机直接调用启动类装载器(而不是通过一次findSystemClass ()调用),而它无法确定或者生成所请求类型的二进制数据,虚拟机抛出NoCIassDefFoundError。

•如果用户自定义的类装载器通过一次findSystemClass ()调用委派给启动类装载器,而启 动类装载器无法确定或者生成所请求的二进制数据,findSystemclass ()方法产生一个 ClassNotFoundError中断。同样,如果用户自定义的类装载器通过loadClass ()调用委派给另一个用户自定义的类装载器,并且用户自定义的类装载器无法确定或者生成所请求类 型的二进制数据,它的loadClass()方法产生ClassNotFoundError中断。

•如果二进制数据被启动类装载器确定或者生成了,怛是它的结构不正确,虚拟机抛出 ClassFormatError异常。同样.如果用户自定义的类装载器能够确定或者生成二进制数据, 并且调用了defineClass ()方法,但是defineClass ()方法发现二进制数据并非合话的结构,defineClass ()会产生一个ClassFormatError中断。

•如果二进制数据被生成了,但是版本号无法识别(比如java class文件的主版本号或者次版 本号太高),虚拟机抛出UnsupportedClassVersionError异常。

•如果二进制数据被生成了而且组织良好,但是在类或者接口之后跟着的并非是所需的名字 (比如文件CuteKitty.cIass被发现包含名为HungryTiger的类而非CuteKitty),虚拟机抛出 NoCIassDefFoundError异常。

•如果组织良好的二进制数据被传递给defineClass (),但是包含的类或者接口的名字已经在当前类装载器的命名空间中存在,defineClass ()方法产生一个LinkageError中断。

•如果类不包含一个超类,并且自己也不是Object类本身,虚拟机抛出ClassFormatError异常 (注意这个检杳在装载这一步完成,因为虚拟机在这一步需要一部分信息——指向超类的 符号索引。在步骤I,虚拟机必须递归地装载所有的超类)。

•如果一个类看上去是它自己的超类,或者一个接口作为自己的接口,虚拟机抛出 ClassCircularityError 异常。

•如果类型引用的超类其实是个接口,或者引用的超接口其实是个类,虚拟机抛出 IncompatibleClassChangeError 异常。

步骤1b:检査访问权限

随着装载结束,虚拟机检查访问权限。如果发起引用的类型没有访问被引用的类型的权限, 虚拟机抛出IllegalAccessError异常。逻辑上说,步骤lb是校验的一部分,但是并非在正式校验阶 段完成。检查访问权限总是在步骤1a之后,以确保符号引用指向的类型被装载进正确的命名空 间,这是解析符号引用的一部分。一旦检查结束,步骤lb以及整个解析CONSTANT_Class_info 人口的过程就结束了。

如果步骤la或者lb发生了错误,符号引用解析就失败了。但是如果在步骤lb权限检查之前一切正常,这个类总体上来说还是可以使用的,只不过不能被发起引用的类型使用。如果错误 在检査权限之前抛出,类型是不可使用的,必须被标记为不可使用或者被取消。

步骤2:连接并初始化类型和任何超类

在这个时候,被解折的、被CONSTANT_class_info人口引用的类型已经被装载了,但是还 没有进行必要的连接和初始化。类型所有超类和超接口也被装载了,但是也没有进行必要的连 接和初始化。某些超类型可能已经被初始化了,因为它们可能是在早期解析过程中被初始化的。

在第7章中讲过,超类必须在子类之前被初始化。如果虚拟机因为主动使用一个类而正在解 析该类(不是接口)的引用,它必须确认它的所有超类都被初始化了,从Object开始沿着继承的 结构向下处理,直到被引用的类(请注意这和步骤la装载的顺序是相反的)。如果一个类型还没 有被连接,在初始化之前必须被连接。请注意只有超类必须被初始化,超接口是不必的。

步骤2a:校验类型

步骤2从第7章描述的正式连接校验阶段开始。第7章曾讲过,校验过程可能要求虚拟机装载 新的类型来确认字节码符合Java语言的语义。比如,如果一个指向特定类的实例的引用被陚给一 个变量,而该变量被声明为不同的类类型,虚拟机可能不得不装载这两种类型,以确认其中一个是另一个的子类。其他的类可能被装载,甚至被连接了,但是肯定不会被初始化。

如果在校验阶段Java虚拟机遇到了麻烦,它会拋出VerifyError异常。

步骤2b:准备类型

随着正式校验阶段的结束,类型必须被准备好。在第7章描述过,在准备阶段虚拟机为类变 量以及随实现不同而有差别的数据结构(比如方法表)分配内存。

步骤2c:可选的步骤,解析类型

在这时候,类型已经被装载、校验也准备好了。按照第7章所介绍的,虚拟机的实现可 以可选地在这时候解析类型。记住,我们现在是在解析过程中的一个阶段,关于一个被引用的类型,步骤la、2a、2b已经解析了发起引用的类型的常量池的CONSTANT_class_info人口。步 骤2c是关于被引用类型(而非发起引用的类型)中所包含的符号引用的解析。(顺便说一句,步 骤2b之前没有被提到被引用类型是因为步骤2b与被引用的类型的装载、连接和初始化过程毫无 关系。步骤2b实际上是发起引用的类型的连接阶段的4次校验中的一部分,发起引用的类型是指 包含指向被引用类型的符号引用的类型。)

举个例子,假若虚拟机正在解析一个从Cat类指向Mouse类的符号引用,虚拟机为Mouse类 执行了步骤la、2a和2b,在从Cat类的常量池中解析指向Mouse的符号引用时,虚拟机可能可选 地(作为步骤2c)解析Mouse类的常量池中的所有符号引用。比如Mouse的常量池中包含一个指 向Cheese类的符号引用,虚拟机这时候可能装载井可选地连接(但并非初始化)Cheese类。虚 拟机不能在这儿试图去初始化Cheese,因为Cheese没有被主动使用。(当然,Cheese可能实际上 早就被装载了,因为在别的地方被主动使用过了。所以可能已经在这个命名空间中被装载、连 接并且初始化过了。)

在本章的前面部分提到过,如果一个实现在解析过程的这个时刻执行步骤2c (早期解析), 它必须在这个符号引用被运行的程序实际使用之前不报告任何错误。比如,如果在解析Mouse类的常量池的过程中,虚拟机无法找到Cheese类,那么它也不会抛出NoClassDefFound错误,除非 Cheese类被程序实际使用。

步驟2d:初始化类型

在这个时刻,类型已经被装载、校验、准备好了,可能也可选地被解析了。经过漫长的过 程,类型终于准备好进行初始化了。按照第7葶中定义的,初始化包括两个步骤。如果类型拥有 任何超类,初始化类型的超类是按照自顶向下的顺序进行的。如果类型有一个类初始化方法, 那也在此时执行。如果类拥有类初始化方法,恰好是在步骤2d执行它。因为步骤2d在所有被引 用的类型的超类上是按照自顶向下的顺序执行的,所以步骤2d会先在超类上执行,而后在子类 上执行。

如果类初始化方法随着抛出某个非Error子类的异常而终止,虚拟机抛出Exception InitializerError异常,把抛出的异常作为构造方法的参数。否则,如果抛出的异常是Error的子类, 虚拟机就抛出那个错误;如果虚拟机因为内存不足而无法创建新的ExceptionInitializerError, 它抛出 OutOfMemoryError 异常。

原文地址:https://www.cnblogs.com/mongotea/p/11979587.html