Java SE 8版序言
第一章 简介
1.1. 一段历史
Java®编程语言是一种通用的、并发的、面向对象的语言。它的语法与C和C++相似,但它省略了许多使C和C++复杂、混乱和不安全的功能。Java平台最初是为了解决为联网的消费设备构建软件的问题而开发的。它被设计为支持多种主机架构,并允许安全地交付软件组件。为了满足这些要求,编译后的代码必须能够在网络上传输,在任何客户端上运行,并向客户端保证其运行的安全性。
万维网的普及使这些属性更加有趣。网络浏览器使数以百万计的人能够上网,并以简单的方式访问丰富的媒体内容。终于有了一种媒介,无论你使用什么机器,无论它是连接到快速网络还是慢速调制解调器,你看到和听到的内容基本上都是一样的。
网络爱好者们很快发现,网络的HTML文档格式所支持的内容太有限了。HTML的扩展,如表格,只是突出了这些限制,同时也清楚地表明,没有一个浏览器可以包括用户想要的所有功能。可扩展性就是答案。
HotJava浏览器首先展示了Java编程语言和平台的有趣特性,使HTML页面内嵌入程序成为可能。程序与出现在其中的HTML页面一起被透明地下载到浏览器中。在被浏览器接受之前,程序被仔细检查,以确保它们是安全的。与HTML页面一样,编译后的程序是独立于网络和主机的。这些程序的行为是相同的,不管它们来自哪里,也不管它们被加载到什么样的机器上运行。
采用Java平台的网络浏览器不再局限于一套预先确定的功能。包含动态内容的网页的访问者可以确信他们的机器不会被这些内容所破坏。程序员只需编写一个程序,就可以在任何提供Java运行环境的机器上运行。
1.2. Java虚拟机
Java虚拟机是Java平台的基石。它是该技术的组成部分,负责其硬件和操作系统的独立性,其编译代码的小尺寸,以及保护用户免受恶意程序的能力。
Java虚拟机是一个抽象的计算机。像一个真正的计算机一样,它有一个指令集,并在运行时操作各种内存区域。使用虚拟机来实现一种编程语言是相当普遍的;最著名的虚拟机可能是UCSD Pascal的P代码机。
Java虚拟机的第一个原型实现是在Sun Microsystems公司完成的,它在一个类似于当代个人数字助理(PDA)的手持设备中模拟了Java虚拟机指令集的软件。甲骨文公司目前的实施方案在移动、桌面和服务器设备上模拟了Java虚拟机,但Java虚拟机并没有假定任何特定的实施技术、主机硬件或主机操作系统。它本身不是解释型的,但同样可以通过将其指令集编译成硅CPU的指令集来实现。它也可以用微码或直接用硅实现。
Java虚拟机对Java编程语言一无所知,只知道一种特殊的二进制格式,即类文件格式。一个类文件包含Java虚拟机指令(或字节码)和一个符号表,以及其他辅助信息。
为了安全起见,Java虚拟机对类文件中的代码施加了强大的语法和结构约束。然而,任何具有可以用有效的类文件表达的功能的语言都可以由Java虚拟机托管。在一个普遍可用的、与机器无关的平台的吸引下,其他语言的实现者可以将Java虚拟机作为其语言的交付工具。
这里指定的 Java 虚拟机与 Java SE 8 平台兼容,并支持《Java 语言规范,Java SE 8 版》中指定的 Java 编程语言。
1.3. 本规范的组织
第2章概述了Java虚拟机的结构。
第3章介绍了将用Java编程语言编写的代码编译到Java虚拟机的指令集中。
第4章规定了类文件格式,这是一种与硬件和操作系统无关的二进制格式,用于表示编译后的类和接口。
第5章规定了Java虚拟机的启动以及类和接口的加载、链接和初始化。
第6章规定了Java虚拟机的指令集,按照操作码记忆法的字母顺序介绍了这些指令。
第7章给出了一个按操作码值索引的Java虚拟机操作码记忆法的表格。
在《Java®虚拟机规范》第二版中,第2章概述了旨在支持Java虚拟机规范的Java编程语言,但它本身并不是该规范的一部分。在《Java虚拟机规范,Java SE 8版》中,读者可以参考《Java语言规范,Java SE 8版》来了解有关Java编程语言的信息。形式的引用。(JLS §x.y)表示有必要这样做的地方。
在《Java®虚拟机规范》第二版中,第8章详细介绍了解释Java虚拟机线程与共享主内存交互的低级动作。在《Java虚拟机规范,Java SE 8版》中,读者可以参考《Java语言规范,Java SE 8版》的第17章,了解有关线程和锁的信息。第17章反映了由JSR 133专家组制作的《Java内存模型和线程规范》。
1.4. 符号
在本规范中,我们提到了来自Java SE平台API的类和接口。每当我们使用单个标识符N来引用一个类或接口(除了那些在示例中声明的类或接口)时,其目的是引用包java.lang中名为N的类或接口。对于java.lang以外的包中的类或接口,我们使用完全限定的名称。
每当我们引用一个在包 java 或其任何子包中声明的类或接口时,目的是引用由 bootstrap 类加载器加载的该类或接口(§5.3.1)。
每当我们引用一个名为java的包的子包时,目的是引用由bootstrap类加载器决定的那个子包。
本规范中的字体使用情况如下。
固定宽度字体用于Java虚拟机数据类型、异常、错误、类文件结构、Prolog代码和Java代码片段。
斜体用于Java虚拟机 “汇编语言”、其操作码和操作数,以及Java虚拟机运行时数据区的项目。它也被用来介绍新的术语和简单的强调。
非规范性信息,旨在澄清规范,以较小的缩进文本给出。
这是非规范性的信息。它提供了直觉、原理、建议和例子等。
第二章 Java虚拟机的结构
本文档规定了一个抽象的机器。它并没有描述Java虚拟机的任何特定实现。
要正确地实现Java虚拟机,你只需要能够读取类文件格式并正确地执行其中规定的操作。不属于Java虚拟机规范的实施细节会不必要地限制实施者的创造力。例如,运行时数据区的内存布局、所使用的垃圾收集算法以及 Java 虚拟机指令的任何内部优化(例如,将其翻译成机器代码)都由实现者自行决定。
本规范中所有对Unicode的引用都是以Unicode标准6.0.0版为依据,可在http://www.unicode.org/。
2.1.类文件格式
将由Java虚拟机执行的编译代码使用与硬件和操作系统无关的二进制格式表示,通常(但不一定)存储在一个文件中,被称为类文件格式。类文件格式精确地定义了类或接口的表示方法,包括诸如字节排序等细节,这些细节在特定平台的对象文件格式中可能是理所当然的。
第4章,“类文件格式”,详细介绍了类文件格式。
2.2.数据类型
像Java编程语言一样,Java虚拟机对两种类型进行操作:原始类型和引用类型。相应地,有两种可以存储在变量中、作为参数传递、由方法返回和操作的值:原始值和引用值。
Java虚拟机希望几乎所有的类型检查都在运行前完成,通常由编译器完成,而不需要由Java虚拟机本身完成。原始类型的值不需要被标记或以其他方式检查,以便在运行时确定其类型,或与引用类型的值区分开来。相反,Java虚拟机的指令集使用旨在对特定类型的值进行操作的指令来区分其操作数类型。例如,iadd、ladd、fadd和dadd都是Java虚拟机指令,它们将两个数字值相加并产生数字结果,但每个指令都是针对其操作数类型的:int、long、float和double。关于Java虚拟机指令集的类型支持摘要,见第2.11.1节。
Java虚拟机包含对对象的明确支持。一个对象是一个动态分配的类实例或一个数组。对一个对象的引用被认为是具有Java虚拟机类型的引用。类型引用的值可以被认为是指向对象的指针。对一个对象的引用可能不止一个。对象总是通过类型引用的值进行操作、传递和测试。
2.3.原始类型和值
Java虚拟机支持的原始数据类型是数字类型、布尔类型(§2.3.4)和返回地址类型(§2.3.3)。
数字类型包括double类型(§2.3.1)和float类型(§2.3.2)。
积分类型有:。
· 字节,其值为8位有符号的二进制整数,其默认值为0
· 短,其值是16位有符号的二补整数,其默认值为0
· int,其值是32位有符号的二进制整数,其默认值为0
· long,其值是64位有符号的二进制整数,其默认值为0
· char,其值为16位无符号整数,代表基本多语言平面中的Unicode码位,用UTF-16编码,其默认值为空码位(’\u0000’)。
浮点类型是:。
· float,其值是float值集的元素,或者在支持的情况下,是float-extended-exponent值集的元素,其默认值是正零。
· double,其值是double值集的元素,或者在支持的情况下,是double-extended-exponent值集的元素,其默认值是正0。
布尔类型的值编码真假值,默认值为false。
第一版的《Java® 虚拟机规范》没有把布尔值看作是一种 Java 虚拟机类型。然而,布尔值在 Java 虚拟机中确实得到了有限的支持。第二版《Java® 虚拟机规范》将布尔值视为一种类型,从而澄清了这个问题。
returnAddress类型的值是指向Java虚拟机指令操作码的指针。在原始类型中,只有returnAddress类型没有与Java编程语言类型直接相关。
2.3.1.积分的类型和值
Java虚拟机的积分类型的值是。
· 对于字节,从-128到127(-27 到27 - 1),包括在内。
· 简而言之,从-32768到32767(-215 到215 - 1),包括在内。
· 对于int,从-2147483648到2147483647(-231 到231 - 1),包括在内。
· 对于长线,从-9223372036854775808到9223372036854775807(-263 到263 - 1),包括在内。
· 对于char,从0到65535(含)。
2.3.2.浮点类型、值集和值
浮点类型是float和double,它们在概念上与32位单精度和64位双精度格式的IEEE 754值和操作相关联,这在IEEE二进制浮点运算标准(ANSI/IEEE Std. 754-1985, New York)中有所规定。
IEEE 754标准不仅包括正负符号大小的数字,还包括正负零、正负无穷大,以及一个特殊的非数字值(以下缩写为 “NaN”)。NaN值用于表示某些无效操作的结果,如零除以零。
Java虚拟机的每个实现都需要支持两个标准的浮点值集,称为浮点值集和双倍值集。此外,Java虚拟机的实现可以选择支持两个扩展指数的浮点值集,即浮点扩展指数值集和双倍扩展指数值集中的一个或两个。在某些情况下,这些扩展指数值集可以代替标准值集来表示float或double类型的值。
任何浮点值集的有限非零值都可以用s⋅m⋅2(e − N + 1) 的形式表示,其中s是+1或-1,m是小于2N的正整数,e是Emin = -(2K−1 -2)和Emax = 2K−1 -1之间的整数,其中N和K是取决于该值集的参数。有些值可以用不止一种方式来表示;例如,假设一个值集中的一个值v可以用s、m和e的某些值来表示,那么ifm是偶数,e小于2K-1 ,我们可以将m减半,e增加1,以产生同一个值v的第二个表示。ifm≥2N-1 ,这种形式的表示被称为规范化;否则表示被称为非规范化。if一个值集中的一个值不能以这样的方式表示,即m≥2N-1 ,那么这个值就被说成是一个非正常化的值,因为它没有正常化的表示。
表2.3.2-A总结了两个必需的和两个可选的浮点值集对参数N和K(以及衍生参数Emin 和Emax )的约束。
表2.3.2-A.浮点值设置参数
| 参数 | 浮动 | 浮动扩展指数(float-extended-exponent | 双 | 双扩展指数 |
|---|---|---|---|---|
| N | 24 | 24 | 53 | 53 |
| K | 8 | ≥ 11 | 11 | ≥ 15 |
| Emax | +127 | ≥ +1023 | +1023 | ≥ +16383 |
| Emin | -126 | ≤ -1022 | -1022 | ≤ -16382 |
if一个实施方案支持一个或两个扩展指数值集,那么对于每个支持的扩展指数值集,都有一个特定的与实施方案相关的常数K,其值受表2.3.2-A的限制;这个值K又决定了Emin 和 Emax 的值。
四个值集中的每一个不仅包括上面赋予它的有限非零值,还包括正零、负零、正无穷、负无穷和NaN五个值。
请注意,表2.3.2-A中的约束条件是这样设计的:浮动值集的每个元素必然也是浮动扩展指数值集、双倍值集和双倍扩展指数值集的元素。同样地,双倍值集的每个元素也必然是双倍扩展指数值集的一个元素。每个扩展指数值集比相应的标准值集有更大的指数值范围,但没有更高的精度。
浮点值集的元素正好是可以用IEEE 754标准中定义的单浮点格式表示的值,除了只有一个NaN值(IEEE 754规定了224 -2个不同的NaN值)。双重值集的元素正是可以用IEEE 754标准中定义的双重浮点格式表示的值,除了只有一个NaN值(IEEE 754规定了253 -2个不同的NaN值)。但是,请注意,这里定义的float-extended-exponent和double-extended-exponent值集的元素并不对应于可以分别使用IEEE 754单扩展和双扩展格式表示的值。除了浮点值必须用类文件格式表示外,本规范并不强制要求浮点值集的数值有特定的表示方法(§4.4.4,§4.4.5)。
float、float-extended-exponent、double 和 double-extended-exponent 值集不是类型。对于 Java 虚拟机的实现来说,使用 float 值集的一个元素来表示 float 类型的值总是正确的;然而,在某些情况下,可能允许实现使用 float-extended-exponent 值集的一个元素来代替。同样,对于一个实现来说,使用double值集的一个元素来表示一个double类型的值总是正确的;但是,在某些情况下,可能允许一个实现使用double-extended-exponent值集的一个元素来代替。
除了NaN,浮点值集的值是有顺序的。当从最小到最大排列时,它们是负无穷大、负有限值、正负零、正有限值和正无穷大。
浮点正零和浮点负零比较起来是相等的,但还有其他操作可以区分它们;例如,1.0除以0.0产生正无穷大,但1.0除以-0.0产生负无穷大。
NaN是无序的,所以数字比较和数字平等的测试,if其操作数中的任何一个或两个都是NaN,则数值为false。特别是,当且仅当一个值是NaN时,对一个值与它本身的数字相等的测试的值是假的。if任何一个操作数是NaN,则数字不平等测试的值为真。
2.3.3.returnAddress的类型和值
returnAddress类型被Java虚拟机的jsr、ret和jsr_w指令使用(§jsr、§ret、§jsr_w)。returnAddress类型的值是指向Java虚拟机指令操作码的指针。与数字原始类型不同,returnAddress类型不对应于任何Java编程语言类型,不能被运行的程序修改。
2.3.4.布尔类型
尽管Java虚拟机定义了一个布尔类型,但它只提供了非常有限的支持。没有专门用于操作布尔值的Java虚拟机指令。相反,Java编程语言中对布尔值进行操作的表达式被编译为使用Java虚拟机int数据类型的值。
Java虚拟机确实直接支持布尔数组。它的newarray指令(§newarray)可以创建布尔数组。布尔类型的数组可以使用字节数组指令baload和bastore(§baload, §bastore)来访问和修改。
在Oracle的Java虚拟机实现中,Java编程语言中的布尔数组被编码为Java虚拟机字节数组,每个布尔元素使用8比特。
Java虚拟机对布尔数组组件进行编码,用1表示真,用0表示假。当Java编程语言的布尔值被编译器映射为Java虚拟机类型int的值时,编译器必须使用相同的编码。
2.4.参考类型和值
有三种引用类型:类类型、数组类型和接口类型。它们的值分别是对动态创建的类实例、数组或实现接口的类实例或数组的引用。
一个数组类型由一个单维的组件类型组成(其长度不是由该类型给出的)。一个数组类型的组成类型本身可以是一个数组类型。if从任何一个数组类型开始,我们考虑它的组成类型,然后(if它也是一个数组类型)考虑该类型的组成类型,以此类推,最终我们必须达到一个不是数组类型的组成类型;这被称为数组类型的元素类型。阵列类型的元素类型必然是一个原始类型,或者一个类别类型,或者一个接口类型。
一个引用值也可以是特殊的null引用,即对任何对象的引用,这里将用null来表示。null引用最初没有运行时类型,但可以被转换为任何类型。一个引用类型的默认值是null。
本规范没有规定具体的数值编码null。
2.5.运行时数据区
Java虚拟机定义了各种运行时数据区,在程序执行期间使用。其中一些数据区域在Java虚拟机启动时被创建,只有在Java虚拟机退出时才会被销毁。其他数据区域是按线程划分的。每线程数据区在创建线程时被创建,并在线程退出时被销毁。
2.5.1.pc寄存器
Java虚拟机可以同时支持许多线程的执行(JLS §17)。每个Java虚拟机线程都有自己的pc(程序计数器)寄存器。在任何时候,每个Java虚拟机线程都在执行一个方法的代码,即该线程的当前方法(§2.6)。if该方法不是本地的,pc寄存器包含当前正在执行的Java虚拟机指令的地址。if线程当前正在执行的方法是本地的,那么Java虚拟机的pc寄存器的值是未定义的。Java虚拟机的pc寄存器的宽度足以容纳一个returnAddress或特定平台上的本地指针。
2.5.2.Java虚拟机堆栈
每个Java虚拟机线程都有一个私有的Java虚拟机栈,与线程同时创建。Java虚拟机堆栈存储帧(§2.6)。Java虚拟机堆栈类似于C语言等传统语言的堆栈:它保存局部变量和部分结果,并在方法调用和返回中发挥作用。因为除了推送和弹出框架外,Java虚拟机堆栈从未被直接操作过,框架可以被堆分配。Java虚拟机堆栈的内存不需要是连续的。
在《Java®虚拟机规范》第一版中,Java虚拟机栈被称为Java栈。
本规范允许 Java 虚拟机堆栈具有固定的大小,或者根据计算的需要动态地扩展和收缩。ifJava虚拟机栈是固定大小的,每个Java虚拟机栈的大小可以在创建该栈时独立选择。
一个Java虚拟机的实现可以为程序员或用户提供对Java虚拟机堆栈初始大小的控制,以及在动态扩展或收缩Java虚拟机堆栈的情况下,对最大和最小大小的控制。
以下是与Java虚拟机堆栈有关的特殊情况。
· if一个线程中的计算需要一个比允许的更大的Java虚拟机堆栈,Java虚拟机会抛出一个StackOverflowError。
· ifJava虚拟机堆栈可以动态扩展,并尝试扩展,但没有足够的内存可以实现扩展,或者没有足够的内存可以为新线程创建初始Java虚拟机堆栈,那么Java虚拟机会抛出OutOfMemoryError。
2.5.3.堆
Java虚拟机有一个堆,在所有Java虚拟机线程之间共享。堆是运行时的数据区,所有的类实例和数组的内存都是从这里分配的。
堆是在虚拟机启动时创建的。对象的堆存储由自动存储管理系统(称为垃圾收集器)回收;对象从未被明确地取消分配。Java虚拟机没有假定特定类型的自动存储管理系统,存储管理技术可以根据实现者的系统要求来选择。堆可以是一个固定的大小,也可以根据计算的需要进行扩展,if没有必要使用更大的堆,也可以收缩。堆的内存不需要是连续的。
一个Java虚拟机的实现可以为程序员或用户提供对堆的初始大小的控制,以及,if堆可以动态扩展或收缩,对最大和最小堆大小的控制。
以下的例外情况与堆有关。
· if一个计算需要的堆超过了自动存储管理系统所能提供的数量,Java虚拟机会抛出OutOfMemoryError。
2.5.4.方法区
Java虚拟机有一个方法区,在所有Java虚拟机线程之间共享。方法区类似于传统语言的编译代码的存储区,或者类似于操作系统进程中的 "文本 "段。它存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括用于类和实例初始化以及接口初始化的特殊方法(第2.9节)。
方法区在虚拟机启动时被创建。尽管方法区在逻辑上是堆的一部分,但简单的实现可以选择不对其进行垃圾收集或压缩。本规范没有规定方法区的位置或用于管理编译代码的策略。方法区可以是一个固定的大小,也可以根据计算的需要进行扩展,if不需要更大的方法区,也可以进行收缩。方法区的内存不需要是连续的。
Java虚拟机的实现可以为程序员或用户提供对方法区初始大小的控制,以及在不同大小的方法区中,对最大和最小方法区大小的控制。
以下是与方法区相关的特殊条件。
· if方法区的内存不能用来满足分配请求,Java虚拟机会抛出OutOfMemoryError。
2.5.5.运行时常量池
运行时常量池是类文件中常量池表的每类或每界面的运行时表示(§4.4)。它包含几种常量,从编译时已知的数字字面到必须在运行时解决的方法和字段引用。运行时常量池的功能类似于传统编程语言的符号表,尽管它包含的数据范围比典型的符号表要广。
每个运行时常量池都是从 Java 虚拟机的方法区分配的(§2.5.4)。一个类或接口的运行时常量池是在Java虚拟机创建该类或接口时构建的(§5.3)。
以下是与类或接口的运行时常量池的构建相关的特殊条件。
· 当创建一个类或接口时,if构建运行时常量池需要的内存超过Java虚拟机的方法区可以提供的内存,Java虚拟机会抛出OutOfMemoryError。
关于运行时常量池的构造,见第5节(加载、链接和初始化)。
2.5.6.本地方法堆栈
Java虚拟机的实现可以使用常规堆栈,俗称 “C堆栈”,以支持本地方法(用Java编程语言以外的语言编写的方法)。本机方法栈也可以由Java虚拟机指令集的解释器实现使用,如C语言。Java虚拟机实现不能加载本机方法,而且本身不依赖传统栈,就不需要提供本机方法栈。if提供,本地方法栈通常在每个线程创建时分配。
本规范允许本地方法堆栈有固定的大小,或者根据计算的需要动态地扩展和收缩。if本地方法栈是固定大小的,每个本地方法栈的大小可以在创建该栈时独立选择。
Java虚拟机实现可以为程序员或用户提供对本地方法栈初始大小的控制,以及在不同大小的本地方法栈的情况下,对最大和最小方法栈大小的控制。
以下是与本地方法栈相关的例外条件。
· if一个线程中的计算需要的本地方法栈大于允许的范围,Java虚拟机会抛出一个StackOverflowError。
· if本地方法栈可以动态扩展,并尝试进行本地方法栈扩展,但没有足够的内存可用,或者没有足够的内存可用于为新线程创建初始本地方法栈,Java虚拟机会抛出OutOfMemoryError。
2.6.框架
一个框架用于存储数据和部分结果,以及执行动态链接、方法的返回值和调度异常。
每当一个方法被调用时就会创建一个新的框架。一个框架在其方法调用完成后被销毁,无论该完成是正常的还是突然的(它抛出了一个未捕获的异常)。帧是从创建帧的线程的Java虚拟机栈(§2.5.2)中分配的。每个框架都有自己的局部变量数组(§2.6.1),自己的操作数栈(§2.6.2),以及对当前方法的类的运行时常量池(§2.5.5)的引用。
一个框架可以用额外的特定实现信息进行扩展,如调试信息。
本地变量数组和操作数栈的大小在编译时确定,并与框架相关的方法的代码一起提供(§4.7.3)。因此,框架数据结构的大小只取决于Java虚拟机的实现,而这些结构的内存可以在方法调用时同时分配。
在一个特定的控制线程中,只有一个框架,即执行方法的框架,在任何时候都是活跃的。这个框架被称为当前框架,其方法被称为当前方法。定义当前方法的类就是当前类。对局部变量和操作数堆栈的操作通常都是参照当前帧进行的。
if一个框架的方法调用了另一个方法,或者它的方法完成了,那么这个框架就不再是当前状态。当一个方法被调用时,一个新的框架被创建,并在控制权转移到新方法时成为当前框架。在方法返回时,当前帧将其方法调用的结果(if有的话)传回给前一帧。然后,当前帧被丢弃,因为前一帧成为当前帧。
请注意,一个线程创建的框架是该线程的局部,不能被任何其他线程所引用。
2.6.1.本地变量
每个框架(§2.6)都包含一个变量数组,称为局部变量。一个框架的局部变量数组的长度在编译时确定,并在类或接口的二进制表示中与框架相关方法的代码一起提供(§4.7.3)。
一个局部变量可以保存布尔、字节、char、short、int、float、reference或returnAddress类型的值。一对局部变量可以容纳一个long或double类型的值。
局部变量是通过索引来处理的。第一个局部变量的索引是0。一个整数被认为是局部变量数组的索引,当且仅当该整数在零和一之间,小于局部变量数组的大小。
一个long类型或double类型的值占据了两个连续的局部变量。这样的值只能用较小的索引来处理。例如,一个存储在本地变量数组中索引为n的double类型的值实际上占据了索引为n和n+1的本地变量;然而,索引为n+1的本地变量不能被加载。它可以被存储进去。然而,这样做会使局部变量n的内容失效。
Java虚拟机并不要求n是偶数。从直观上看,long和double类型的值在局部变量数组中不需要进行64位对齐。实现者可以自由决定使用为该值保留的两个局部变量来表示这种值的适当方式。
Java虚拟机使用局部变量来传递方法调用的参数。在类方法的调用中,任何参数都从局部变量0开始在连续的局部变量中传递。在实例方法的调用中,局部变量0总是用来传递对实例方法被调用的对象的引用(这在Java编程语言中)。随后,任何参数都从局部变量1开始在连续的局部变量中传递。
2.6.2.操作数栈
每个框架(§2.6)都包含一个后进先出(LIFO)的堆栈,称为其操作数堆栈。一个框架的操作数栈的最大深度在编译时确定,并与框架相关的方法的代码一起提供(§4.7.3)。
在上下文清楚的情况下,我们有时会把当前帧的操作数栈简单地称为操作数栈。
当包含它的框架被创建时,操作数栈是空的。Java虚拟机提供指令,将常量或局部变量或字段的值加载到操作数栈上。其他Java虚拟机指令从操作数栈中获取操作数,对其进行操作,并将结果推回操作数栈。操作数栈也被用来准备传递给方法的参数和接收方法的结果。
例如,iadd指令(§iadd)将两个int值加在一起。它要求被添加的int值是操作数堆栈的前两个值,由以前的指令推到那里。两个int值都从操作数栈中弹出。它们被相加,然后它们的总和被推回到操作数堆栈中。子计算可以嵌套在操作数栈上,产生的值可以被包含的计算所使用。
操作数栈上的每个条目都可以容纳任何Java虚拟机类型的值,包括长类型或双类型的值。
操作数堆栈中的值必须以适合其类型的方式进行操作。例如,不可能推送两个int值,然后把它们当作一个long,或者推送两个float值,然后用iadd指令把它们加起来。一小部分Java虚拟机指令(dup指令(§dup)和swap(§swap))将运行时数据区域作为原始值进行操作,而不考虑其具体类型;这些指令的定义方式是不能用来修改或分解单个值。这些对操作数堆栈操作的限制是通过类文件验证来执行的(§4.10)。
在任何时候,一个操作数栈都有一个相关的深度,其中long或double类型的值对深度有两个单位的贡献,任何其他类型的值有一个单位的贡献。
2.6.3.动态链接
每个框架(§2.6)包含对当前方法类型的运行时常量池(§2.5.5)的引用,以支持方法代码的动态链接。一个方法的类文件代码指的是要被调用的方法和要通过符号引用访问的变量。动态链接将这些符号化的方法引用转化为具体的方法引用,必要时加载类以解决尚未定义的符号,并将变量访问转化为与这些变量的运行时位置相关的存储结构中的适当偏移量。
这种对方法和变量的晚期绑定使得一个方法所使用的其他类中的变化不太可能破坏这段代码。
2.6.4.正常的方法调用完成
if一个方法的调用没有导致一个异常(§2.10)被抛出,无论是直接从Java虚拟机还是执行显式抛出语句的结果,那么该方法的调用就正常完成。if当前方法的调用正常完成,那么可以向调用的方法返回一个值。这发生在被调用的方法执行返回指令之一的时候(§2.11.8),该指令的选择必须适合被返回值的类型(if有的话)。
在这种情况下,当前帧(§2.6)被用来恢复调用者的状态,包括它的局部变量和操作栈,调用者的程序计数器被适当增加,以跳过方法调用指令。然后在调用方法的框架内继续正常执行,并将返回值(if有的话)推到该框架的操作数栈中。
2.6.5.突然的方法调用完成
if在方法中执行一个 Java 虚拟机指令导致 Java 虚拟机抛出一个异常(§2.10),并且该异常没有在方法中被处理,那么方法调用就会突然完成。执行 athrow 指令(§athrow)也会导致明确抛出一个异常,if该异常没有被当前方法捕获,则会导致方法调用的突然完成。一个突然完成的方法调用不会向其调用者返回一个值。
2.7.对象的表示
Java虚拟机没有规定对象的任何特定内部结构。
在Oracle的一些Java虚拟机实现中,对类实例的引用是一个指向句柄的指针,该句柄本身是一对指针:一个指向包含对象的方法的表和代表对象类型的类对象的指针,另一个指向从堆中为对象数据分配的内存。
2.8.浮点算术
Java虚拟机包含了IEEE二进制浮点运算标准(ANSI/IEEE Std. 754-1985, New York)中规定的浮点运算的一个子集。
2.8.1.Java虚拟机浮点运算和IEEE 754
Java虚拟机支持的浮点运算与IEEE 754标准的主要区别是:。
· Java虚拟机的浮点操作不会抛出异常、陷阱或以其他方式发出无效操作、除以零、溢出、下溢或不精确等IEEE 754例外条件的信号。Java虚拟机没有发出NaN值的信号。
· Java虚拟机不支持IEEE 754标志的浮点比较。
· Java虚拟机的四舍五入操作总是使用IEEE 754四舍五入模式。不精确的结果被四舍五入到最接近的可表示的值,并列的值是最小有效位为零的。这是IEEE 754的默认模式。但Java虚拟机指令将浮点类型的值转换为积分类型的值时,会向零舍入。Java虚拟机没有提供任何方法来改变浮点的舍入模式。
· Java虚拟机不支持IEEE 754单一扩展格式或双重扩展格式,除非双重和双重扩展指数值集可以说是支持单一扩展格式。可以选择支持的float-extended-exponent和double-extended-exponent值集并不对应于IEEE 754扩展格式的值:IEEE 754扩展格式需要扩展精度和扩展指数范围。
2.8.2.浮点模式
每个方法都有一个浮点模式,要么是FP-strict,要么不是FP-strict。一个方法的浮点模式由定义该方法的method_info结构(§4.6)的access_flags项的ACC_STRICT标志的设置决定。设置了这个标志的方法是FP-strict,否则,该方法就不是FP-strict。
请注意,ACC_STRICT标志的这种映射意味着由JDK 1.1版或更早版本的编译器编译的类中的方法实际上不是FP-strict。
当调用的方法创建了包含操作栈的框架时,我们会把操作栈称为具有特定的浮点模式。同样地,当包含一个Java虚拟机指令的方法具有特定的浮点模式时,我们会将该指令称为具有该浮点模式。
if支持float-extended-exponent值集(§2.3.2),在不受FP限制的操作栈上的float类型的值可以在该值集上取值,除非被值集转换所禁止(§2.8.3)。if支持双倍扩展指数值集(§2.3.2),那么在非FP-strict的操作栈上的双倍类型的值可以在该值集上范围,除非被值集转换所禁止。
在所有其他情况下,不管是在操作数堆栈还是其他地方,也不管是什么浮点模式,浮点类型的值只能在浮点值集和双倍值集范围内。特别是,类和实例字段、数组元素、局部变量和方法参数只能包含来自标准值集的数值。
2.8.3.值集转换
支持扩展浮点值集的 Java 虚拟机实现允许或要求在特定情况下,在扩展值集和标准值集之间映射相关浮点类型的值。这种值集转换不是类型转换,而是与同一类型相关的值集之间的映射。
在表明值集转换的地方,允许一个实现对一个值进行以下操作之一。
· if该值是浮动类型,并且不是浮动值集的一个元素,它将该值映射到浮动值集的最近的元素。
· if值是双倍类型,并且不是双倍值集的一个元素,那么它将值映射到双倍值集的最近的元素。
此外,在表明有价值集转换的地方,需要进行某些操作。
· 假设执行一条非FP-strict的Java虚拟机指令会导致一个float类型的值被推到一个FP-strict的操作栈上,作为参数传递,或存储到一个局部变量、一个字段或一个数组中的元素。if该值不是float值集的一个元素,它将该值映射到最近的float值集的一个元素。
· 假设执行一条非FP-strict的Java虚拟机指令会导致一个双倍类型的值被推入一个FP-strict的操作栈,作为参数传递,或存储到一个局部变量、一个字段或一个数组的元素中。if该值不是double值集的一个元素,它会将该值映射到最近的double值集的一个元素。
这种必要的值集转换可能发生在方法调用过程中传递浮点类型的参数,包括本地方法的调用;从非FP-strict的方法向FP-strict的方法返回浮点类型的值;或者在非FP-strict的方法中将浮点类型的值存储到局部变量、字段或数组中。
并非所有来自扩展指数值集的值都能准确地映射到相应的标准值集中的一个值。if一个被映射的值过大而不能准确表示(它的指数大于标准值集所允许的指数),它将被转换为相应类型的(正或负)无穷大。if一个被映射的值太小而不能准确表示(它的指数小于标准值集所允许的指数),它将被四舍五入到最接近的可表示的去正化值或相同符号的零。
值集转换保留了无穷大和NaN,并且不能改变被转换值的符号。值集转换对不属于浮点类型的值没有影响。
2.9.特殊方法
在Java虚拟机的层面上,每一个用Java编程语言编写的构造函数(JLS §8.8)都作为一个实例初始化方法出现,它有一个特殊的名字< init>。这个名字是由编译器提供的。因为< init>这个名字不是一个有效的标识符,它不能直接用于用Java编程语言编写的程序中。实例初始化方法只能在Java虚拟机中通过invokespecial指令(§invokespecial)来调用,而且它们只能在未初始化的类实例上调用。实例初始化方法拥有它所派生的构造函数的访问权限(JLS §6.6)。
一个类或接口最多只有一个类或接口的初始化方法,并通过调用该方法进行初始化(§5.5)。一个类或接口的初始化方法有一个特殊的名字< clinit>,不需要参数,并且是无效的(§4.3.3)。
类文件中名为< clinit>的其他方法是没有意义的。它们不是类或接口的初始化方法。它们不能被任何Java虚拟机指令所调用,也不会被Java虚拟机本身所调用。
在一个版本号为51.0或以上的类文件中,该方法必须另外设置ACC_STATIC标志(§4.6),才能成为类或接口的初始化方法。
这一要求是在Java SE 7中引入的。在一个版本号为50.0或以下的类文件中,一个名为< clinit>的方法if是空的,并且不需要任何参数,则被认为是类或接口的初始化方法,无论其ACC_STATIC标志如何设置。
< clinit>这个名字是由编译器提供的。因为< clinit>这个名字不是一个有效的标识符,它不能直接用于用Java编程语言编写的程序中。类和接口的初始化方法是由Java虚拟机隐式调用的;它们从未被任何Java虚拟机指令直接调用,而只是作为类初始化过程的一部分间接调用。
if以下情况都是真的,一个方法就是签名多态的。
· 它被声明在java.lang.invoke.MethodHandle类中。
· 它有一个类型为Object[]的单一形式参数。
· 它的返回类型是Object。
· 它设置了ACC_VARARGS和ACC_NATIVE标志。
在Java SE 8中,唯一的签名多态方法是java.lang.invoke.MethodHandle类的invoke和invokeExact方法。
Java虚拟机在invokevirtual指令(§invokevirtual)中对签名多态方法进行了特殊处理,以实现对方法句柄的调用。方法句柄是一个强类型的、可直接执行的对底层方法、构造函数、字段或类似底层操作的引用(§5.4.3.5),可选择对参数或返回值进行转换。这些转换是相当普遍的,包括转换、插入、删除和替换等模式。更多信息见Java SE平台API中的java.lang.invoke包。
2.10.例外情况
Java虚拟机中的异常是由Throwable类或其子类中的一个实例表示的。抛出一个异常会导致控制权从抛出异常的地方立即非本地转移。
大多数异常都是同步发生的,是由发生异常的线程的动作导致的。相比之下,异步异常有可能发生在程序执行的任何时候。Java虚拟机抛出异常的原因有三个。
· 执行了一条弃权指令(§athrow)。
· Java虚拟机同步检测到一个异常的执行条件。这些异常不是在程序中的任意一点抛出的,而是在执行一条指令后才同步抛出的,这条指令要么。
o 指定异常为可能的结果,如:。
§ 当指令体现的操作违反了Java编程语言的语义,例如在数组的边界之外进行索引。
§ 当加载或链接部分程序时发生错误。
o 导致资源的某些限制被超过,例如,当使用了太多的内存。
· 发生了一个异步异常,因为。
o 调用了Thread或ThreadGroup类的停止方法,或
o 在Java虚拟机实现中发生了一个内部错误。
停止方法可以由一个线程调用,以影响另一个线程或指定线程组中的所有线程。它们是异步的,因为它们可能发生在其他线程或线程执行的任何时刻。一个内部错误被认为是异步的(§6.3)。
在抛出异步异常之前,Java虚拟机可能会允许少量的但有限制的执行发生。允许这种延迟是为了让优化后的代码能够检测并抛出这些异常,同时遵守Java编程语言的语义。
一个简单的实现可能会在每个控制传输指令的点上对异步异常进行轮询。由于程序的大小是有限的,这就为检测异步异常的总延迟提供了一个约束。由于在控制传输之间不会发生异步异常,代码生成器有一定的灵活性,可以在控制传输之间重新安排计算,以提高性能。建议进一步阅读Marc Feeley撰写的论文Polling Efficiently on Stock Hardware, Proc. 1993 Conference on Functional Programming and Computer Architecture, Copenhagen, Denmark, pp.179-187。
由Java虚拟机抛出的异常是精确的:当控制权的转移发生时,在抛出异常的点之前执行的所有指令的效果必须看起来已经发生了。在抛出异常的时间点之后发生的任何指令都不能显示为已经被评估了。if优化后的代码推测性地执行了异常发生点之后的一些指令,那么这些代码必须准备将这种推测性的执行从用户可见的程序状态中隐藏起来。
Java虚拟机中的每个方法都可以与零个或多个异常处理程序相关联。一个异常处理程序指定了实现该方法的 Java 虚拟机代码的偏移量范围,该异常处理程序是有效的,描述了该异常处理程序能够处理的异常类型,并指定了处理该异常的代码的位置。if引起异常的指令的偏移量在异常处理程序的偏移量范围内,并且异常类型与异常处理程序处理的异常类别相同或为其子类,则该异常与异常处理程序相匹配。当一个异常被抛出时,Java虚拟机会在当前方法中搜索一个匹配的异常处理程序。if找到了一个匹配的异常处理程序,系统就会分支到匹配的处理程序所指定的异常处理代码。
if在当前方法中没有找到这样的异常处理程序,那么当前方法的调用就会突然完成(§2.6.5)。在突然完成时,当前方法调用的操作栈和局部变量被丢弃,其框架被弹出,恢复了调用方法的框架。然后在调用者框架的上下文中重新抛出异常,以此类推,沿着方法调用链继续下去。if在到达方法调用链的顶端之前没有找到合适的异常处理程序,那么抛出异常的线程的执行将被终止。
方法的异常处理程序被搜索匹配的顺序是很重要的。在一个类文件中,每个方法的异常处理程序被存储在一个表中(§4.7.3)。在运行时,当一个异常被抛出时,Java 虚拟机会按照它们在类文件中相应的异常处理程序表中出现的顺序搜索当前方法的异常处理程序,从该表的开始。
请注意,Java虚拟机并不强制对方法的异常表项进行嵌套或任何排序。Java编程语言的异常处理语义只通过与编译器合作来实现(§3.12)。当类文件通过其他方式生成时,定义的搜索程序可以确保所有的 Java 虚拟机实现将表现得一致。
2.11.指令集摘要
一个Java虚拟机指令由一个字节的操作码组成,指定要执行的操作,然后是零个或多个操作数,提供操作所使用的参数或数据。许多指令没有操作数,只由一个操作码组成。
忽略异常情况,Java虚拟机解释器的内循环实际上就是
做{
以原子方式计算PC,并在PC上获取操作码。
if(操作数)获取操作数。
执行该操作码的动作。
} while (还有更多事情要做)。
操作数的数量和大小由操作码决定。if一个操作数的大小超过一个字节,那么它将以大-序存储–高阶字节优先。例如,一个无符号的16位局部变量的索引被存储为两个无符号的字节,byte1和byte2,这样它的值就是(byte1 << 8)| byte2。
字节码指令流只有单字节对齐。两个例外是lookupswitch和tableswitch指令(§lookupswitch, §tableswitch),它们被填充以强制它们的一些操作数在4字节边界上的内部对齐。
将Java虚拟机的操作码限制为一个字节,并在编译后的代码中放弃数据对齐,这一决定反映了对紧凑性的有意识的偏爱,可能在天真的实现中以一些性能为代价。一个字节的操作码也限制了指令集的大小。不假定数据对齐意味着在许多机器上,大于一个字节的即时数据必须在运行时由字节构成。
2.11.1.类型和Java虚拟机
Java虚拟机指令集中的大多数指令都对其执行的操作进行了类型信息编码。例如,iload指令(§iload)将一个局部变量(必须是int)的内容加载到操作数栈中。fload指令(§fload)对一个浮点数做了同样的操作。这两条指令可能有相同的实现方式,但有不同的操作码。
对于大多数类型的指令,指令的类型在操作码记忆法中用一个字母明确表示:i表示int操作,l表示long,s表示short,b表示byte,c表示char,f表示float,d表示double,a表示 reference。一些类型不明确的指令在其助记符中没有类型字母。例如,arraylength总是对一个数组对象进行操作。一些指令,如goto,一个无条件的控制转移,不对类型操作数进行操作。
鉴于Java虚拟机的操作码大小为一个字节,将类型编码到操作码中对其指令集的设计造成了压力。if每个类型的指令都支持Java虚拟机的所有运行时数据类型,那么将有更多的指令可以用一个字节表示。相反,Java虚拟机的指令集为某些操作提供了较低水平的类型支持。换句话说,该指令集有意地不是正交的。必要时,可以使用单独的指令在不支持的和支持的数据类型之间进行转换。
Table 2.11.1-A概述了Java虚拟机指令集中的类型支持。带有类型信息的特定指令是通过将操作码列中的指令模板中的T替换成类型列中的字母来建立的。if某个指令模板和类型的类型列是空的,那么就不存在支持该类型操作的指令。例如,有一条针对int类型的加载指令iload,但没有针对字节类型的加载指令。
请注意,表2.11.1-A中的大多数指令都没有字节(byte)、字符(char)和短文(short)的积分类型的形式。没有一个指令有布尔类型的形式。编译器使用Java虚拟机指令对byte和short类型的字面值的加载进行编码,这些指令在编译时或运行时将这些值符号扩展为int类型的值。布尔和char类型的字面值的加载使用指令进行编码,这些指令在编译时或运行时将字面值零扩展到int类型的值。同样地,从布尔型、字节型、短型和char型的数组中加载数值时,使用Java虚拟机指令进行编码,该指令将数值符号扩展或零扩展到int型的数值。因此,对实际类型为boolean、byte、char和short的值的大多数操作都由对计算类型为int的值进行操作的指令正确执行。
表2.11.1-A.Java虚拟机指令集中的类型支持
| opcode | byte | short | int | long | float | double | char | reference |
|---|---|---|---|---|---|---|---|---|
| Tipush | bipush | sipush | ||||||
| Tconst | iconst | lconst | fconst | dconst | aconst | |||
| Tload | iload | lload | fload | dload | aload | |||
| Tstore | istore | lstore | fstore | dstore | astore | |||
| Tinc | iinc | |||||||
| Taload | baload | saload | iaload | laload | faload | daload | caload | aaload |
| Tastore | bastore | sastore | iastore | lastore | fastore | dastore | castore | aastore |
| Tadd | iadd | ladd | fadd | dadd | ||||
| Tsub | isub | lsub | fsub | dsub | ||||
| Tmul | imul | lmul | fmul | dmul | ||||
| Tdiv | idiv | ldiv | fdiv | ddiv | ||||
| Trem | irem | lrem | frem | drem | ||||
| Tneg | ineg | lneg | fneg | dneg | ||||
| Tshl | ishl | lshl | ||||||
| Tshr | ishr | lshr | ||||||
| Tushr | iushr | lushr | ||||||
| Tand | iand | land | ||||||
| Tor | ior | lor | ||||||
| Txor | ixor | lxor | ||||||
| i2T | i2b | i2s | i2l | i2f | i2d | |||
| l2T | l2i | l2f | l2d | |||||
| f2T | f2i | f2l | f2d | |||||
| d2T | d2i | d2l | d2f | |||||
| Tcmp | lcmp | |||||||
| Tcmpl | fcmpl | dcmpl | ||||||
| Tcmpg | fcmpg | dcmpg | ||||||
| if_TcmpOP | if_icmpOP | if_acmpOP | ||||||
| Treturn | ireturn | lreturn | freturn | dreturn | areturn |
表2.11.1-B概述了Java虚拟机实际类型和Java虚拟机计算类型之间的映射。
某些Java虚拟机指令,如pop和swap,在操作数堆栈上操作而不考虑类型;然而,这些指令被限制为只能用于某些类别的计算类型的值,也在表2.11.1-B中给出。
表2.11.1-B.Java虚拟机中的实际类型和计算类型
| Actual type | Computational type | Category |
|---|---|---|
| boolean | int | 1 |
| byte | int | 1 |
| char | int | 1 |
| short | int | 1 |
| int | int | 1 |
| float | float | 1 |
| reference | reference | 1 |
| returnAddress | returnAddress | 1 |
| long | long | 2 |
| double | double | 2 |
2.11.2.加载和存储指令
加载和存储指令在本地变量(§2.6.1)和Java虚拟机框架(§2.6.2)的操作数栈(§2.6)之间传输数值。
· 将一个局部变量加载到操作栈中:iload, iload_ , lload, lload_ , fload, fload_ , dload, dload_ , aload, aload_。
· 将操作数堆栈中的一个值存储到一个局部变量中: istore, istore_ , lstore, lstore_ , fstore, fstore_ , dstore, dstore_ , astore, astore_。
· 将一个常数加载到操作栈中:bipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_m1, iconst_, lconst_, fconst_, dconst_。
· 使用更宽的索引获得对更多局部变量的访问,或访问更大的即时操作数:宽。
访问对象的字段和数组元素的指令(§2.11.5)也会将数据传入和传出操作数栈。
上面显示的角括号中带有尾部字母的指令符号(例如,iload_)表示指令家族(在iload_的情况下,成员有iload_0、iload_1、iload_2和iload_3)。这样的指令家族是一个额外的通用指令(iload)的特殊化,它需要一个操作数。对于专门的指令,操作数是隐含的,不需要存储或获取。其他方面的语义是相同的(iload_0的意思与操作数为0的iload相同)。角括号中的字母指定了该系列指令的隐式操作数的类型:对于,非负整数;对于,int;对于,long;对于,float;以及对于,double。在许多情况下,int类型的形式被用来对byte、char和short类型的值进行操作(§2.11.1)。
这种指令族的符号在本规范中一直使用。
2.11.3.算术指令
算术指令计算一个结果,这个结果通常是操作数栈中两个值的函数,将结果推回操作数栈。有两种主要的算术指令:操作整数值的指令和操作浮点值的指令。在这两种指令中,算术指令都是专门针对Java虚拟机的数字类型。对于字节、短和char类型的值(§2.11.1)或布尔类型的值,不直接支持整数算术;这些操作由操作int类型的指令来处理。整数和浮点指令在溢出和除以0的行为上也有所不同。算术指令如下。
· 添加:iadd, ladd, fadd, dadd。
· 减法:isub, lsub, fsub, dsub。
· 乘法:imul, lmul, fmul, dmul。
· 分割:IDIV,LIV,FIV,DIV。
· 其余的:Irem, lrem, frem, drem。
· 负数:ineg, lneg, fneg, dneg。
· 移位:Ishl, ishr, iushr, lshl, lshr, lushr。
· Bitwise OR: ior, lor.
· Bitwise AND: iand, land.
· 位数排他性OR:ixor, lxor。
· 本地变量增量:iinc。
· 比较:dcmpg, dcmpl, fcmpg, fcmpl, lcmp。
Java编程语言对整数和浮点值的运算符的语义(JLS §4.2.2, JLS §4.2.4)直接由Java虚拟机指令集的语义支持。
在对整数数据类型进行操作时,Java虚拟机不会显示溢出。唯一可以抛出异常的整数操作是整数除法指令(idiv和ldiv)和整数余数指令(irem和lrem),if除数为零,则抛出ArithmeticException。
Java虚拟机对浮点数的操作是按照IEEE 754的规定进行的。特别是,Java虚拟机要求完全支持IEEE 754的去规范化浮点数和渐进下溢,这使得证明特定数字算法的理想属性更加容易。
Java虚拟机要求浮点运算的行为就像每个浮点运算符将其浮点结果四舍五入到结果精度一样。不精确的结果必须被四舍五入到最接近无限精确结果的可表示值;if两个最接近的可表示值同样接近,则选择最小有效位为0的那个。这是IEEE 754标准的默认四舍五入模式,被称为四舍五入模式。
在将浮点值转换为整数时,Java虚拟机使用IEEE 754四舍五入模式。这将导致数字被截断;代表操作数的小数部分的任何位都被丢弃。化整为零模式选择最接近但不大于无限精确结果的类型值作为其结果。
Java虚拟机的浮点运算符不抛出运行时异常(不要与IEEE 754浮点异常相混淆)。溢出的操作产生一个有符号的无穷大,溢出不足的操作产生一个非正常化的值或有符号的零,而没有数学上明确结果的操作产生NaN。所有以NaN为操作数的数字运算都会产生NaN的结果。
对长类型的值的比较(lcmp)执行有符号比较。对浮点类型的值(dcmpg, dcmpl, fcmpg, fcmpl)的比较使用IEEE 754非信令比较进行。
2.11.4.类型转换说明
类型转换指令允许在Java虚拟机的数字类型之间进行转换。这些指令可用于在用户代码中实现明确的转换,或缓解Java虚拟机指令集中正交性的不足。
Java虚拟机直接支持以下拓宽的数字转换。
· 从int到long, float, 或double
· 长条形改为浮动或双倍
· 浮点数转为双数
扩大的数字转换指令有i2l、i2f、i2d、l2f、l2d和f2d。考虑到类型化指令的命名惯例和2表示 "到 "的双关语,这些操作码的记忆法是很简单的。例如,i2d指令将一个int值转换为一个双数。
大多数加宽的数字转换不会丢失关于数字值的整体大小的信息。事实上,从int到long和int到double的转换根本不会丢失任何信息;数字值被准确地保留下来。从float到double的转换是FP-strict的(§2.8.2),也完全保留了数值;只有这种非FP-strict的转换才可能失去关于转换后数值的总体大小的信息。
从int转换到float,或者从long转换到float,或者从long转换到double,可能会损失精度,也就是说,可能会损失一些最小有效位的值;所得到的浮点值是整数值的正确舍入版本,使用IEEE 754舍入到最近的模式。
尽管可能会出现精度损失,但加宽数字转换绝不会导致Java虚拟机抛出运行时异常(不要与IEEE 754浮点异常相混淆)。
将一个int转换为一个长的简单符号,扩大了int值的二补表示,以填补更宽的格式。一个char的扩大数字转换为一个积分类型的零,扩大了char值的表示,以填补更宽的格式。
请注意,从积分类型byte、char和short到int类型不存在加宽的数字转换。正如在§2.11.1中指出的,字节、char和short类型的值在内部被加宽到int类型,使得这些转换是隐含的。
Java虚拟机还直接支持以下缩小的数字转换。
· 从int到byte、short或char
· 从长到短
· 将浮点数转为英数或长数
· 将双倍数转换成英数、长数或浮点数
缩小的数字转换指令是i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。缩小的数字转换可能导致不同的符号、不同的数量级,或者两者都有;因此可能会失去精度。
将int或long缩小到一个积分类型T的数字转换,只需丢弃除最低阶的n位以外的所有位,其中n是用于表示类型T的位数。这可能会导致结果值与输入值的符号不一致。
在将一个浮点值缩小到一个积分类型T的数字转换中,其中T是int或long,浮点值的转换方法如下。
· if浮点值是NaN,则转换的结果是int或long 0。
· 否则,if浮点值不是无穷大,则使用IEEE 754向零舍入模式将浮点值舍入为整数值V。有两种情况。
o ifT是长,并且这个整数值可以表示为长,那么结果就是长值V。
o ifT是int类型的,并且这个整数值可以表示为int,那么结果就是int值V。
· 否则。
o 要么数值必须太小(大的负值或负无穷大),结果是int或long类型的最小可表示值。
o 或者该值必须太大(一个大的正值或正无穷大),结果是int或long类型的最大可表示值。
从double到float的缩小数字转换的行为符合IEEE 754的规定。结果使用IEEE 754的四舍五入模式被正确舍入。一个太小而不能表示为float的值被转换为float类型的正或负零;一个太大而不能表示为float的值被转换为正或负无穷大。一个双倍的NaN总是被转换为一个浮点数的NaN。
尽管可能会发生溢出、下溢或精度损失,但数字类型之间的缩小转换绝不会导致Java虚拟机抛出一个运行时异常(不要与IEEE 754浮点异常相混淆)。
2.11.5.对象的创建和操作
尽管类实例和数组都是对象,但Java虚拟机使用不同的指令集创建和操作类实例和数组。
· 创建一个新的类实例:new。
· 创建一个新的数组:newarray, anewarray, multianewarray。
· 访问类的字段(静态字段,称为类变量)和类实例的字段(非静态字段,称为实例变量):getstatic、putstatic、getfield、putfield。
· 将一个数组组件加载到操作数栈中:baload, caload, saload, iaload, laload, faload, daload, aaload。
· 将操作数栈中的一个值存储为数组组件:bastore, castore, sastore, iastore, lastore, fastore, dastore, aastore。
· 获取数组的长度:arraylength。
· 检查类实例或数组的属性:instanceof,checkcast。
2.11.6.操作数栈管理指令
提供了一些直接操作操作数栈的指令:pop, pop2, dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2, swap.
2.11.7.控制权转移指令
控制转移指令有条件地或无条件地使Java虚拟机继续执行除控制转移指令之后的其他指令。它们是
· 条件性分支:ifeq, ifne, iflt, ifle, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icmple, if_icmpgt if_icmpge, if_acmpeq, if_acmpne。
· 复合条件分支:tablewitch,lookupswitch。
· 无条件分支: goto, goto_w, jsr, jsr_w, ret.
Java虚拟机有不同的指令集,在与int和引用类型的数据进行比较时有条件地进行分支。它也有不同的条件性分支指令来测试空引用,因此不需要为null指定一个具体的值(§2.4)。
在布尔型、字节型、char型和short型数据之间进行比较的条件性分支是用int比较指令进行的(§2.11.1)。在long、float或double类型的数据之间进行比较的条件性分支是通过比较数据并产生一个int比较结果的指令开始的(§2.11.3)。随后的int比较指令测试这个结果,并实现条件分支。由于强调int比较,Java虚拟机为int类型提供了丰富的条件性分支指令的补充。
所有的int条件控制转移指令都进行有符号比较。
2.11.8.方法调用和返回指令
以下五条指令调用了方法。
· invokevirtual调用一个对象的实例方法,对该对象的(虚拟)类型进行调度。这就是Java编程语言中正常的方法调度。
· invokeinterface调用一个接口方法,搜索特定运行时对象实现的方法以找到合适的方法。
· invokespecial调用一个需要特殊处理的实例方法,无论是实例初始化方法(§2.9)、私有方法,还是超类方法。
· invokestatic在一个命名的类中调用一个类(静态)方法。
· invokedynamic调用的方法是与invokedynamic指令绑定的调用点对象的目标。调用站点对象被Java虚拟机绑定到invokedynamic指令的特定词法发生上,这是在第一次执行该指令之前运行引导方法的结果。因此,invokedynamic指令的每一次出现都有一个独特的链接状态,与其他调用方法的指令不同。
方法的返回指令按返回类型区分,有ireturn(用于返回布尔、字节、char、short或int类型的值)、lreturn、freturn、dreturn和areturn。此外,返回指令还用于从声明为无效的方法、实例初始化方法以及类或接口初始化方法中返回。
2.11.9.抛出异常
异常是通过使用atrow指令以编程方式抛出的。异常也可以由各种Java虚拟机指令抛出,if它们检测到异常情况。
2.11.10.同步化
Java虚拟机通过一个单一的同步结构支持方法和方法中的指令序列的同步:监视器。
方法级同步是隐式执行的,作为方法调用和返回的一部分(§2.11.8)。在运行时常量池的method_info结构(§4.6)中,同步方法是通过ACC_SYNCHRONIZED标志来区分的,该标志由方法调用指令检查。当调用一个ACC_SYNCHRONIZED被设置的方法时,执行线程会进入一个监视器,调用该方法本身,并退出监视器,无论该方法的调用是正常完成还是突然发生。在执行线程拥有监视器期间,其他线程不得进入该监视器。if在调用同步方法的过程中抛出了一个异常,而同步方法没有处理该异常,那么在异常被重新抛出同步方法之前,该方法的监视器会自动退出。
指令序列的同步化通常被用来编码Java编程语言的同步块。Java虚拟机提供monitorenter和monitorexit指令来支持这种语言结构。同步块的正确实现需要针对Java虚拟机的编译器的配合(§3.14)。
结构化锁定是指在方法调用期间,给定监视器上的每个出口都与该监视器上的前一个条目相匹配的情况。由于不能保证所有提交给Java虚拟机的代码都会执行结构化锁定,所以Java虚拟机的实现允许但不要求执行以下两条保证结构化锁定的规则。让T是一个线程,M是一个监视器。那么。
\1. 无论方法调用是正常完成还是突然完成,在方法调用期间,T在M上执行的监控条目的数量必须等于T在方法调用期间在M上执行的监控退出的数量。
\2. 在方法调用过程中,T在方法调用后对M执行的监控退出次数不得超过T在方法调用后对M执行的监控进入次数。
请注意,当调用一个同步方法时,Java虚拟机自动执行的监视器进入和退出被认为是发生在调用方法的调用过程中。
2.12.类库
Java虚拟机必须为实现Java SE平台的类库提供足够的支持。if没有Java虚拟机的配合,这些库中的一些类是无法实现的。
可能需要Java虚拟机特别支持的类包括那些支持。
· 反射,如包java.lang.reflect中的类和类。
· 加载和创建一个类或接口。最明显的例子是ClassLoader类。
· 类或接口的链接和初始化。上面引用的例子类也属于这个范畴。
· 安全性,如包java.security中的类和其他类,如SecurityManager。
· 多线程,如Thread类。
· 弱引用,如java.lang.ref.包中的类。
上面的列表是为了说明问题而不是全面的。对这些类或它们所提供的功能的详尽列表超出了本规范的范围。详情请参见Java SE平台类库的规范。
2.13.公共设计,私人实施
到目前为止,本规范已经勾勒出Java虚拟机的公共视图:类文件格式和指令集。这些组件对Java虚拟机的硬件、操作系统和实现的独立性都至关重要。实施者可能更愿意把它们看作是在每个实施Java SE平台的主机之间安全地交流程序片段的一种手段,而不是看作是一个需要严格遵循的蓝图。
了解公共设计和私人实现之间的界限是很重要的。Java虚拟机的实现必须能够读取类文件,并且必须完全实现其中的Java虚拟机代码的语义。做到这一点的一个方法是把这个文件作为一个规范,并按字面意思来实现这个规范。但对于实现者来说,在本规范的约束下修改或优化实现也是完全可行和可取的。只要类文件格式能够被读取,其代码的语义能够被保持,实现者就可以用任何方式实现这些语义。只要认真维护正确的外部接口,"引擎盖下 "的东西是实现者的事。
有一些例外情况:调试器、剖析器和即时代码生成器都需要访问通常被认为是 "在引擎罩下 "的 Java 虚拟机元素。在适当的时候,甲骨文公司与其他Java虚拟机实施者和工具供应商合作,开发Java虚拟机的通用接口,供这些工具使用,并在整个行业推广这些接口。
实现者可以利用这种灵活性,为高性能、低内存使用或可移植性定制Java虚拟机的实现。在一个特定的实现中,什么是有意义的,取决于该实现的目标。实现选项的范围包括以下内容。
· 在加载时或执行时将Java虚拟机代码翻译成另一个虚拟机的指令集。
· 在加载时或执行期间将Java虚拟机代码翻译成主机CPU的本地指令集(有时被称为即时代码生成,或JIT)。
精确定义的虚拟机和对象文件格式的存在不需要大大限制实现者的创造力。Java虚拟机被设计为支持许多不同的实现,提供新的和有趣的解决方案,同时保留实现之间的兼容性。
第三章 为Java虚拟机编译
Java虚拟机机器是为支持Java编程语言而设计的。甲骨文公司的JDK软件包含一个从用Java编程语言编写的源代码到Java虚拟机指令集的编译器,以及一个实现Java虚拟机本身的运行时间系统。了解一个编译器是如何利用 Java 虚拟机的,对于未来的编译器编写者以及试图了解 Java 虚拟机本身的人来说,都是很有用的。本章中编号的部分不是规范性的。
请注意,"编译器 "一词有时是指从Java虚拟机的指令集到特定CPU的指令集的翻译器。这种翻译器的一个例子是及时(JIT)代码生成器,它只在Java虚拟机代码被加载后生成特定平台的指令。本章不讨论与代码生成有关的问题,只讨论与将用Java编程语言编写的源代码编译为Java虚拟机指令有关的问题。
3.1.例子的格式
本章主要包括源代码的示例,以及 Oracle JDK 1.0.2 版中的 javac 编译器为这些示例生成的 Java 虚拟机代码的注释列表。Java 虚拟机代码是用 Oracle 的 javap 工具输出的非正式 "虚拟机汇编语言 "编写的,该工具随 JDK 版本一起发布。你可以使用javap来生成更多的编译方法的例子。
阅读过汇编代码的人应该对这些例子的格式很熟悉。每条指令的形式如下
<index> <opcode> [ <operand1> [ <operand2>...]][<评语>]
是数组中指令的操作码的索引,包含了这个方法的Java虚拟机代码的字节数。另外,也可以看作是从方法开始的一个字节偏移。是该指令操作码的助记符,零个或多个是该指令的操作数。可选的是以行末注释的语法给出的。
8 bipush 100 // 推送int常数100
评论中的一些材料是由javap发出的,其余的是由作者提供的。每条指令前面的可以作为控制转移指令的目标。例如,goto 8指令将控制权转移到索引8的指令。请注意,Java虚拟机控制转移指令的实际操作数是这些指令的操作码地址的偏移量;这些操作数由javap显示(并在本章中显示),作为更容易读入其方法的偏移量。
我们在代表运行时常量池索引的操作数前加一个哈希符号,并在指令后加一个注释,以确定所引用的运行时常量池项目,如:。
10 ldc #1 // 推进浮点常数100.0
或。
9 invokevirtual #4 // Method Example.addTwo(II)I
在本章中,我们并不担心指定诸如操作数大小的细节。
3.2.常量、局部变量和控制结构的使用
Java虚拟机代码表现出一组由Java虚拟机的设计和类型的使用所强加的一般特征。在第一个例子中,我们遇到了许多这样的情况,我们对它们进行了一些详细的考虑。
旋转方法只是围绕一个空的for循环旋转100次。
空白的旋转() {
int i;
for (i = 0; i < 100; i++) {
; // 循环体是空的
}
}
一个编译器可能会将自旋编译为。
0 iconst_0 // 推进int常数0
1 istore_1 // 存储到本地变量1(i=0)。
2 goto 8 // 第一遍不要增量。
5 iinc 1 1 // 将局部变量1增加1 (i++)
8 iload_1 // 推送本地变量1(i)。
9 bipush 100 // 推送int常数100
11 if_icmplt 5 // if小于(i < 100),则进行比较和循环。
14 return // 完成后返回无效
Java虚拟机是面向堆栈的,大多数操作都是从Java虚拟机当前框架的操作数堆栈中获取一个或多个操作数,或者将结果推回到操作数堆栈中。每次调用一个方法都会创建一个新的框架,并随之创建一个新的操作数栈和一组局部变量供该方法使用(§2.6)。在计算的任何一个点上,每个控制线程可能会有许多框架和同样多的操作数栈,对应于许多嵌套的方法调用。只有当前帧中的操作数栈是活动的。
Java虚拟机的指令集通过使用不同的字节码对其不同的数据类型进行操作来区分操作数类型。方法spin只对int类型的值进行操作。在它的编译代码中选择对类型数据进行操作的指令(iconst_0, istore_1, iinc, iload_1, if_icmplt)都是专门针对int类型的。
旋转中的两个常数,0和100,是用两条不同的指令推入操作数堆栈的。0是用iconst_0指令推送的,这是iconst_< i>指令系列中的一条。100是用bipush指令推送的,该指令将其推送的值作为一个即时操作数获取。
Java虚拟机经常利用某些操作数(在iconst_< i>指令中是int常数-1、0、1、2、3、4和5)的可能性,使这些操作数隐含在操作码中。因为iconst_0指令知道它要推送一个int 0,iconst_0不需要存储一个操作数来告诉它要推送什么值,也不需要获取或解码一个操作数。将0的推送编译为bipush 0是正确的,但会使编译后的自旋代码长一个字节。一个简单的虚拟机也会花费额外的时间来获取和解码每次循环的显式操作数。使用隐式操作数可以使编译后的代码更加紧凑和高效。
spin中的int i被存储为Java虚拟机局部变量1。因为大多数Java虚拟机指令都是对从操作栈中弹出的值进行操作,而不是直接对局部变量进行操作,所以在为Java虚拟机编译的代码中,在局部变量和操作栈之间转移值的指令很常见。这些操作在指令集中也有特殊支持。在spin中,使用istore_1和iload_1指令将值转移到本地变量中,每条指令都隐含地对本地变量1进行操作。istore_1指令从操作数堆栈中弹出一个int,并将其存入局部变量1中。iload_1指令将局部变量1中的值推到操作栈中。
本地变量的使用(和重用)是编译器编写者的责任。专门的加载和存储指令应该鼓励编译器编写者在可行的情况下尽可能地重用局部变量。由此产生的代码更快、更紧凑,在框架中使用的空间也更少。
Java虚拟机对局部变量的某些非常频繁的操作进行了专门的处理。iinc指令将一个局部变量的内容以一个字节的有符号值递增。旋转中的iinc指令将第一个局部变量(其第一个操作数)增加1(其第二个操作数)。iinc指令在实现循环结构时非常方便。
自旋的for循环主要是由这些指令完成的。
5 iinc 1 1 // 将局部变量1增加1 (i++)
8 iload_1 // 推送本地变量1(i)。
9 bipush 100 // 推送int常数100
11 if_icmplt 5 // if小于(i < 100),则进行比较和循环。
bipush指令将数值100作为一个int推入操作栈,然后if_icmplt指令将该数值从操作栈中弹出并与i进行比较。否则,控制权转移到if_icmplt之后的指令。
ifspin例子中的循环计数器使用了int以外的数据类型,编译后的代码必然会发生变化,以反映不同的数据类型。例如,if旋转的例子使用的不是int,而是double,如图所示。
空白的dspin() {
双I。
for (i = 0.0; i < 100.0; i++) {
; // 循环体是空的
}
}
编译后的代码是。
方法 void dspin()
0 dconst_0 // 推进双常数 0.0
1 dstore_1 // 存储到本地变量1和2中。
2 goto 9 // 第一遍不要增量。
5 dload_1 // 推送本地变量1和2
6 dconst_1 // 推进双常数1.0
7 dadd // 添加;没有dinc指令
8 dstore_1 // 将结果存储在本地变量1和2中。
9 dload_1 // 推送本地变量1和2
10 ldc2_w #4 // 推动双常数100.0
13 dcmpg // 没有if_dcmplt指令
14 iflt 5 // if小于(i < 100.0),则进行比较和循环。
17 return // 完成后返回无效
对类型数据进行操作的指令现在都是专门针对double类型的。(ldc2_w指令将在本章后面讨论)。
回顾一下,双倍值占据了两个局部变量,尽管它们只能使用两个局部变量中较小的索引来访问。对于long类型的值也是如此。再举例说明。
doubleLocals(double d1, double d2) {
返回d1 + d2。
}
成为
方法 doubleLocals(double,double)
0 dload_1 // 本地变量1和2中的第一个参数
1 dload_3 // 本地变量3和4中的第二个参数
2 dadd
3 dreturn
注意,在doubleLocals中用于存储双倍值的局部变量对的局部变量决不能单独操作。
Java虚拟机的操作码大小为1字节,因此其编译后的代码非常紧凑。然而,1字节的操作码也意味着Java虚拟机的指令集必须保持很小。作为一种妥协,Java虚拟机没有对所有数据类型提供同等的支持:它不是完全正交的(表2.11.1-A)。
例如,在示例spin的for语句中,int类型的值的比较可以用一条if_icmplt指令来实现;但是,在Java虚拟机指令集中没有一条指令可以对double类型的值执行条件分支。因此,dspin必须使用dcmpg指令和iflt指令来实现对double类型的值的比较。
Java虚拟机对int类型的数据提供了最直接的支持。这部分是为了有效实现Java虚拟机的操作数栈和局部变量数组。这也是出于典型程序中int数据出现频率的考虑。其他积分类型得到的直接支持较少。例如,没有字节、字符或短版本的存储、加载或添加指令。下面是用short写的旋转例子。
空白的sspin() {
短短的一。
for (i = 0; i < 100; i++) {
; // 循环体是空的
}
}
它必须为Java虚拟机进行编译,如下所示,使用对另一种类型(很可能是int)进行操作的指令,必要时在短值和int值之间进行转换,以确保对短数据的操作结果保持在适当的范围内。
方法 void sspin()
0 iconst_0
1 istore_1
2得到10
5 iload_1 // 短文被当作是一个int。
6 iconst_1
7 ǞǞǞ
8 i2s // 将int截断为short
9 istore_1
10 ILOAD_1
11个双脉冲100
13 if_icmplt 5
16return
在Java虚拟机中缺乏对字节、char和short类型的直接支持并不特别令人痛苦,因为这些类型的值在内部被提升为int(字节和short被符号扩展为int,char被零扩展)。因此,对字节、char和short数据的操作可以用int指令完成。唯一的额外成本是将int操作的值截断到有效范围。
长点和浮点类型在Java虚拟机中具有中等水平的支持,只缺少完整的条件控制传输指令。
3.3.算术
Java虚拟机通常在其操作数栈上进行算术。(iinc指令是个例外,它直接增加一个局部变量的值)。例如,align2grain方法将一个int值对齐到给定的2的幂。
int align2grain(int i, int grain) {
返回((i + grain-1)& ~(grain-1))。
}
算术运算的操作数从操作数栈中弹出,而运算的结果则推回操作数栈。因此,算术子计算的结果可以作为其嵌套计算的操作数来使用。例如,~(grain-1)的计算是由这些指令处理的。
5 iload_2 // 推动谷物
6 iconst_1 // 推进int常数1
7 isub // 减法;推送结果
8 iconst_m1 // 推送int常数-1
9 ixor // 做XOR;推送结果
第一粒1是用局部变量2的内容和一个即时的int值1计算的。这些操作数从操作数堆栈中弹出,其差值被推回操作数堆栈中。因此,差值可以立即作为ixor指令的一个操作数使用。(记得~x == -1^x.) 同样的,ixor指令的结果成为后续iand指令的一个操作数。
整个方法的代码如下。
方法int align2grain(int,int)。
0 iload_1
1 iload_2
2 iadd
3 iconst_1
4 isub
5 iload_2
6 iconst_1
7 isub
8 iconst_m1
9 ixor
10 iand
11 ireturn
3.4.访问运行时常量库
许多数字常量,以及对象、字段和方法,都是通过当前类的运行时常量池访问的。对象访问将在后面考虑(第3.8节)。int、long、float和double类型的数据,以及对String类实例的引用,都是通过ldc、ldc_w和ldc2_w指令管理的。
ldc和ldc_w指令用于访问运行时常量池(包括String类的实例)中除double和long类型以外的值。ldc_w指令仅在有大量运行时常量池项目,并且需要较大的索引来访问一个项目时才会代替ldc。ldc2_w指令用于访问所有类型为double和long的值;没有非wide的变体。
字节、char或short类型的整数常量,以及小的int值,可以使用bipush、sipush或iconst_< i>指令进行编译(§3.2)。某些小的浮点常量可以使用fconst_和dconst_指令进行编译。
在所有这些情况下,编译是直接的。例如,以下的常数
void useManyNumeric() {
int i = 100;
int j = 1000000;
long l1 = 1;
long l2 = 0xffffffff;
double d = 2.2;
...do some calculations...
}
设置如下。
方法 void useManyNumeric()
0 bipush 100 // 用bipush推送小的int常数
2 istore_1
3 ldc #1 // 用ldc推送大的int常数(1000000)。
5 istore_2
6 lconst_1 // 一个微小的长值使用小的快速 lconst_1
7 lstore_3
8 ldc2_w #6 // 推送长的0xffffffffff(即一个int-1)。
// 可以用ldc2_w推送任何长的常量值
11 lstore 5
13 ldc2_w #8 // 推动双常数2.200000
// 不常见的双倍值也会用ldc2_w推送。
16 dstore 7
...做这些计算...
3.5.更多控制实例
for语句的编译在前面一节(§3.2)中展示过。大多数Java编程语言的其他控制结构(if-then-else、do、while、break和continue)也以明显的方式进行编译。开关语句的编译在另一节中处理(第3.10节),还有异常的编译(第3.12节)和最后条款的编译(第3.13节)。
作为进一步的例子,虽然Java虚拟机提供的具体控制传输指令因数据类型而异,但一个while循环的编译方式是显而易见的。像往常一样,对int类型的数据有更多的支持,例如。
void whileInt() {
int i = 0;
while (i < 100) {
i++;
}
}
被编译为。
方法 void whileInt()
0 iconst_0
1 istore_1
2 goto得到8
5 iinc 1 1
8 Iload_1
9 bipush100
11 if_icmplt 5
14 return
请注意,while语句的测试(使用if_icmplt指令实现)位于循环的Java虚拟机代码的底部。(在前面的旋转例子中也是如此。)测试在循环的底部,迫使我们在循环的第一次迭代之前使用goto指令来进行测试。if该测试失败,而循环体从未进入,这条额外的指令就被浪费了。然而,while 循环通常是在其主体被期望运行时使用的,通常是多次迭代。对于后续的迭代,把测试放在循环的底部,每次循环都可以节省一条Java虚拟机指令:if测试在循环的顶部,循环体就需要一条尾随的Goto指令才能回到顶部。
涉及其他数据类型的控制结构以类似的方式进行编译,但必须使用这些数据类型的可用指令。这导致代码的效率有所下降,因为需要更多的Java虚拟机指令,例如。
void whileDouble() {
double i = 0.0;
while (i < 100.1) {
i++;
}
}
被编译为。
方法 void whileDouble()
0 dconst_0
1 dstore_1
2 goto9
5 dload_1
6 dconst_1
7 dadd
8 dstore_1
9 dload_1
10 ldc2_w #4 // 推动双常数100.1
13 dcmpg // 为了比较和分支,我们必须使用...
14 iflt 5 // ...两条指令
17 return
每种浮点类型都有两条比较指令:fcmpl和fcmpg用于浮点类型,dcmpl和dcmpg用于双点类型。这些变体只在对NaN的处理上有所不同。NaN是无序的(§2.3.2),所以if操作数是NaN,所有浮点比较都会失败。编译器为适当的类型选择比较指令的变体,无论比较在非NaN值上失败还是遇到NaN,都会产生相同的结果。比如说。
int lessThan100(double d) {
if (d < 100.0) {
return 1;
} else {
return -1;
}
}
编译成。
方法int lessThan100(double)。
0 dload_1
1 ldc2_w #4 // 推动双常数100.0
4 dcmpg // ifd是NaN或者d>100.0就推1。
//推0,ifd == 100.0
5 ifge 10 // 在0或1上的分支
8 iconst_1
9 ireturn
10 iconst_m1
11 ireturn
ifd不是NaN并且小于100.0,dcmpg指令将一个int-1推入操作栈,ifge指令不进行分支。无论d是否大于100.0或为NaN,dcmpg指令都将一个int 1推到操作数堆栈中,ifge分支。ifd等于100.0,dcmpg指令将一个int 0推入操作栈,ifge分支。
dcmpl指令在反向比较的情况下也能达到同样的效果。
int greaterThan100(double d) {
if (d > 100.0) {
return 1;
} else {
return -1;
}
}
成为。
方法int greaterThan100(double)。
0 dload_1
1 ldc2_w #4 // 推动双常数100.0
4 dcmpl // ifd是NaN或d<100.0,则推-1。
//推0,ifd == 100.0
5 ifle 10 // 在0或-1上进行分支
8 iconst_1
9 ireturn
10 iconst_m1
11 ireturn
再一次,不管是在非NaN值上比较失败,还是因为传递了一个NaN,dcmpl指令都会把一个int值推到操作数栈上,导致ifle分支。if这两条dcmp指令都不存在,那么其中一个例子方法将不得不做更多的工作来检测NaN。
3.6.接收参数
if有n个参数被传递给一个实例方法,按照惯例,它们将被接收到为新方法调用而创建的框架中编号为1到n的局部变量中。这些参数是按照它们被传递的顺序来接收的。比如说。
int addTwo(int i, int j) {
return i + j;
}
编译成。
方法int addTwo(int,int)。
0 iload_1 // 推送本地变量1的值(i)。
1 iload_2 // 推送本地变量2的值(j)。
2 iadd // 加法;在操作数栈上留下int结果
3 ireturn // 返回int result
按照惯例,一个实例方法在局部变量0中传递一个对其实例的引用。在Java编程语言中,实例可以通过this关键字访问。
类(静态)方法没有实例,所以对它们来说,使用局部变量0是不必要的。一个类方法从索引0开始使用局部变量。ifaddTwo方法是一个类方法,它的参数将以类似于第一个版本的方式传递。
static int addTwoStatic(int i, int j) {
return i + j;
}
编译成。
Method int addTwoStatic(int,int)
0 iload_0
1 iload_1
2 iadd
3 ireturn
唯一的区别是,方法参数从局部变量0开始出现,而不是1。
3.7.调用方法
实例方法的普通方法调用在对象的运行时类型上进行分配。(用C++的话说,它们是虚拟的。)这样的调用是用invokevirtual指令实现的,它的参数是一个运行时常量池条目的索引,它给出了对象的类类型的二进制名称的内部形式,要调用的方法的名称,以及该方法的描述符(§4.3.3)。为了调用前面定义为实例方法的addTwo方法,我们可以这样写。
int add12and13() {
return addTwo(12, 13);
}
这可以编译成。
方法 int add12and13()
0 aload_0 // 推送本地变量0(this)。
1 bipush 12 // 推送int常数12
3 bipush 13 // 推送int常数13
5 invokevirtual #4 // Method Example.addtwo(II)I
8 ireturn // 在操作数堆栈的顶部返回int。
// 它是addTwo()的int结果。
调用的设置是首先将对当前实例的引用,即this,推送到操作数栈中。然后,方法调用的参数,int值12和13被推入。当addTwo方法的框架被创建时,传递给该方法的参数成为新框架的局部变量的初始值。也就是说,被调用者推入操作数堆栈的this和两个参数的引用,将成为被调用方法的局部变量0、1和2的初始值。
最后,addTwo被调用。当它返回时,它的int返回值被推到调用者框架的操作数栈中,即add12和13方法。这样,返回值就可以立即返回给add12和13的调用者。
add12和13的返回是由add12和13的ireturn指令处理的。ireturn指令将addTwo返回的int值,放在当前帧的操作栈中,并将其推送到调用者帧的操作栈中。然后,它将控制权返回给调用者,使调用者的帧成为当前帧。Java虚拟机为它的许多数字和引用数据类型提供了不同的返回指令,也为没有返回值的方法提供了返回指令。同一组返回指令用于所有种类的方法调用。
invokevirtual指令的操作数(在这个例子中,运行时常量池索引#4)不是类实例中方法的偏移量。编译器不知道一个类实例的内部布局。相反,它生成了对一个实例的方法的符号引用,这些方法被存储在运行时常量池中。这些运行时常量池项目在运行时被解析以确定实际的方法位置。所有其他访问类实例的Java虚拟机指令也是如此。
调用addTwoStatic,一个addTwo的类(静态)变体,也是类似的,如图所示。
int add12and13() {
return addTwoStatic(12, 13);
}
尽管使用的是不同的Java虚拟机方法调用指令。
方法 int add12and13()
0 bipush 12
2个bipush 13
4 invokestatic #3 // 方法 Example.addTwoStatic(II)I
7 ireturn
编译一个类(静态)方法的调用与编译一个实例方法的调用非常相似,只是调用者不传递这个方法。因此,方法参数将从局部变量0开始接收(§3.6)。invokestatic指令总是被用来调用类方法。
invokespecial指令必须用于调用实例初始化方法(§3.8)。在调用超类(super)中的方法和调用私有方法时也会用到它。例如,给定类Near和Far声明为。
class Near {
int it;
public int getItNear() {
return getIt();
}
private int getIt() {
return it;
}
}
class Far extends Near {
int getItFar() {
return super.getItNear();
}
}
的方法Near.getItNear(调用一个私有方法)变成。
Method int getItNear()
0 aload_0
1 invokespecial #5 // Method Near.getIt()I
4 ireturn
Far.getItFar方法(调用一个超类方法)变成了。
Method int getItFar()
0 aload_0
1 invokespecial #4 // Method Near.getItNear()I
4 ireturn
请注意,使用invokespecial指令调用的方法总是将其作为第一个参数传递给被调用的方法。像往常一样,它是在局部变量0中接收的。
为了调用一个方法句柄的目标,编译器必须形成一个方法描述符,记录实际的参数和返回类型。编译器不能对参数进行方法调用转换;相反,它必须根据它们自己未转换的类型将它们推入堆栈。编译器安排一个对方法句柄对象的引用,像往常一样,在参数之前被推入堆栈。编译器发出一条invokevirtual指令,该指令引用一个描述符,描述符描述了参数和返回类型。通过对方法解析的特殊安排(§5.4.3.3),调用java.lang.invoke.MethodHandle的invokeExact或invoke方法的invokevirtual指令将始终链接,前提是方法描述符的语法形式良好,并且描述符中命名的类型可以被解析。
3.8.使用类实例的工作
Java虚拟机类实例是使用Java虚拟机的new指令创建的。回顾一下,在Java虚拟机的层面上,构造函数以编译器提供的名称< init>的方法出现。这个特别命名的方法被称为实例初始化方法(§2.9)。一个给定的类可能存在多个实例初始化方法,对应于多个构造函数。一旦类的实例被创建,它的实例变量,包括该类和它的所有超类的变量,都被初始化为它们的默认值,新类实例的实例初始化方法就会被调用。比如说
Object create() {
return new Object();
}
编译成。
Method java.lang.Object create()
0 new #1 // Class java.lang.Object
3 dup
4 invokespecial #4 // Method java.lang.Object.<init>()V
7 areturn
类实例的传递和返回(作为引用类型)非常像数值,尽管引用类型有自己的补充指令,例如。
int i; // An instance variable
MyObj example() {
MyObj o = new MyObj();
return silly(o);
}
MyObj silly(MyObj o) {
if (o != null) {
return o;
} else {
return o;
}
}
成为。
Method MyObj example()
0 new #2 // Class MyObj
3 dup
4 invokespecial #5 // Method MyObj.<init>()V
7 astore_1
8 aload_0
9 aload_1
10 invokevirtual #4 // Method Example.silly(LMyObj;)LMyObj;
13 areturn
Method MyObj silly(MyObj)
0 aload_1
1 ifnull 6
4 aload_1
5 areturn
6 aload_1
7 areturn
类实例的字段(实例变量)是通过getfield和putfield指令来访问的。ifi是一个int类型的实例变量,方法setIt和getIt,定义为。
void setIt(int value) {
i = value;
}
int getIt() {
return i;
}
成为。
Method void setIt(int)
0 aload_0
1 iload_1
2 putfield #4 // Field Example.i I
5 return
Method int getIt()
0 aload_0
1 getfield #4 // Field Example.i I
4 ireturn
与方法调用指令的操作数一样,putfield和getfield指令的操作数(运行时常量池索引#4)不是类实例中字段的偏移量。编译器会生成对实例字段的符号引用,这些字段被保存在运行时常量池中。这些运行时常量池项在运行时被解析,以确定被引用对象中字段的位置。
3.9.数组
Java虚拟机的数组也是对象。数组是用一组不同的指令来创建和操作的。newarray指令用于创建一个数字类型的数组。这条代码。
void createBuffer() {
int buffer[];
int bufsz = 100;
int value = 12;
buffer = new int[bufsz];
buffer[10] = value;
value = buffer[11];
}
可能会被编译成。
方法 void createBuffer()
0 bipush 100 // 推送int常数100(bufsz)。
2 istore_2 // 在本地变量中存储bufsz 2
3 bipush 12 // 推送int常数12(值)。
5 istore_3 // 在本地变量3中存储数值
6 iload_2 // 推动bufsz...
7 newarray int // ...并创建该长度的新int数组
9 astore_1 // 在缓冲区中存储新数组
10 aload_1 // 推进缓冲区
11 bipush 10 // 推送int常数10
13 iload_3 // 推入值
14 iastore // 在缓冲区[10]存储数值。
15 aload_1 // 推进缓冲区
16 bipush 11 // 推送int常数11
18 iaload // 在缓冲区[11]推送数值...
19 istore_3 // ...并将其存储在值中
20 return
例如,anewarray指令用于创建一个对象引用的一维数组。
void createThreadArray() {
Thread threads[];
int count = 10;
threads = new Thread[count];
threads[0] = new Thread();
}
成为。
方法 void createThreadArray()
0 bipush 10 // 推送int常数10
2 istore_2 // 初始化计数为该值
3 iload_2 // 推送计数,由anewarray使用。
4 anewarray class #1 // 创建新的Thread类数组
7 astore_1 // 将新的数组存储在线程中
8 aload_1 // 推送线程的值
9 iconst_0 // 推进int常数0
10 new #1 // 创建线程类的实例
13 dup // 做重复的参考...
14 invokespecial #5 // ...用于线程的构造函数
// 方法 java.lang.Thread.< init>()V
17 aastore // 将新线程存储在数组的0处
18 return
anewarray指令也可以用来创建一个多维数组的第一维。另外,multianewarray指令可以用来一次创建多个维度。例如,三维数组。
int[][][] create3DArray() {
int grid[][][];
grid = new int[10][5][];
return grid;
}
是由以下人员创建的。
方法int create3DArray()[][][] 。
0 bipush 10 // 推送int 10(维度一)。
2 iconst_5 // 推进int 5(二维)。
3 multianewarray #1 dim #2 // Class [[I, a three-dimensional
// int数组;只创建
// 前两个维度
7 astore_1 // 存储新的数组...
8 aload_1 // ...然后准备返回。
9 areturn
multianewarray指令的第一个操作数是运行时常量池对要创建的数组类型的索引。第二个是实际要创建的数组类型的维数。multianewarray指令可以用来创建该类型的所有维数,如create3DArray的代码所示。注意,多维数组只是一个对象,所以分别由load_1和areturn指令加载和返回。关于数组类名的信息,见§4.4.1。
所有的数组都有相关的长度,可以通过arraylength指令访问。
3.10.编译开关
开关语句的编译使用tableswitch和lookupswitch指令。当开关的情况可以有效地表示为目标偏移表的索引时,就会使用tablewitch指令。if开关的表达式的值超出了有效索引的范围,则使用开关的默认目标。例如。
int chooseNear(int i) {
switch (i) {
case 0: return 0;
case 1: return 1;
case 2: return 2;
default: return -1;
}
}
编译成。
方法int chooseNear(int)。
0 iload_1 // 推送本地变量1(参数i)。
1 表开关0到2://有效指数为0到2
0: 28 // ifi是0,在28处继续。
1: 30 // ifi是1,在30处继续。
2: 32 // ifi是2,在32处继续。
默认:34 // 否则,在34处继续
28 iconst_0 // i是0;推送int常数0...。
29 ireturn // ...并将其返回
30 iconst_1 // i was 1; push int constant 1...。
31 ireturn // ...并将其返回
32 iconst_2 // i是2;推送int常数2。
33 ireturn // ...并将其返回
34 iconst_m1 // 否则推送int常数-1...
35 ireturn // ...并将其返回
Java 虚拟机的 tablewitch 和 lookupswitch 指令只对 int 数据进行操作。因为对字节、char或short值的操作在内部被提升为int,一个表达式评估为这些类型之一的开关在编译时就像它评估为int类型一样。ifselectNear方法是用short类型编写的,那么生成的Java虚拟机指令将与使用int类型时相同。其他数字类型必须缩小到int类型才能在开关中使用。
当开关的情况很稀疏时,tablewitch指令的表格表示在空间上变得很低效。可以使用lookupswitch指令来代替。lookupswitch指令将int键(案例标签的值)与表中的目标偏移量配对。当一条lookupswitch指令被执行时,开关的表达式的值与表中的键进行比较。if其中一个键与表达式的值相匹配,则在相关的目标偏移处继续执行。if没有匹配的键,则在默认的目标处继续执行。例如,编译后的代码为。
int chooseFar(int i) {
switch (i) {
case -100: return -1;
case 0: return 0;
case 100: return 1;
default: return -1;
}
}
除了lookupswitch指令外,看起来就像selectNear的代码。
方法选择远方(int)。
0 负载_1
1个lookupswitch 3:
-100: 36
0: 38
100: 40
default:42
36 iconst_m1
37 ireturn
38 iconst_0
39 ireturn
40 iconst_1
41 ireturn
42 iconst_m1
43 ireturn
Java虚拟机规定lookupswitch指令的表必须按键排序,这样实现就可以使用比线性扫描更有效的搜索。即便如此,lookupswitch指令必须搜索它的键来寻找匹配,而不是像tableswitch那样简单地执行边界检查和索引到一个表中。因此,在空间考虑允许的情况下,tablewitch指令可能比lookupswitch更有效。
3.11.对操作数栈的操作
Java虚拟机有大量的指令,可以将操作数堆栈的内容作为非类型值进行操作。这些指令非常有用,因为Java虚拟机依赖于对其操作数栈的巧妙操作。比如说。
public long nextIndex() {
return index++;
}
private long index = 0;
被编译为。
方法 long nextIndex()
0 aload_0 // 推动这个
1 dup // 制作一份副本
2 getfield #4 // 这个副本中的一个被消耗了
//推送长的字段索引。
//在原来的这个上面
5 dup2_x1 // 在操作数堆栈顶部的长条是
//插入到操作数堆栈的下面。
// 原有的这个
6 lconst_1 // 推送长常数1
7 ladd // 索引值被递增...
8 putfield #4 // ......并将结果存储在字段中。
11 lreturn // 指数的原始值是在
// 操作数堆栈,准备被返回
注意,Java虚拟机从不允许其操作数堆栈操作指令修改或分解操作数堆栈上的单个值。
3.12.抛出和处理异常
异常是使用throw关键字从程序中抛出的。它的编译很简单。
void cantBeZero(int i) throws TestExc {
if (i == 0) {
throw new TestExc();
}
}
成为。
方法 void cantBeZero(int)
0 iload_1 // 推送参数1(i)。
1 ifne 12 // ifi==0,分配实例并抛出
4 new #1 // 创建TestExc的实例
7 dup // 一个引用进入其构造函数
8 invokespecial #7 // Method TestExc.< init>()V
11 athrow // 第二次引用被抛出
12 return // if我们抛出了TestExc,就永远不会到这里
对try-catch结构的编译是直接的。比如说。
void catchOne() {
try {
tryItOut();
} catch (TestExc e) {
handleExc(e);
}
}
被编译为。
方法 void catchOne()
0 aload_0 // 尝试块的开始
1 invokevirtual #6 // Method Example.tryItOut()V
4 return // 尝试块的结束;正常返回
5 astore_1 // 将抛出的值存储在本地var 1中。
6 aload_0 // 推动这个
7 aload_1 // 推送抛出值
8 invokevirtual #5 // 调用处理程序方法。
// Example.handleExc(LTestExc;)V
11 return // 处理完TestExc后返回
异常表。
从到目标类型
0 4 5类TestExc
仔细观察,try块的编译方式与不存在try的情况下的编译方式一样。
方法 void catchOne()
0 aload_0 // 尝试块的开始
1 invokevirtual #6 // Method Example.tryItOut()V
4 return // 尝试块的结束;正常返回
if在执行try块的过程中没有抛出异常,它的行为就像try不存在一样:tryItOut被调用,catchOne返回。
在try块之后是实现单一catch子句的Java虚拟机代码。
5 astore_1 // 将抛出的值存储在本地var 1中。
6 aload_0 // 推动这个
7 aload_1 // 推送抛出值
8 invokevirtual #5 // 调用处理程序方法。
// Example.handleExc(LTestExc;)V
11 return // 处理完TestExc后返回
异常表。
从到目标类型
0 4 5类TestExc
handleExc的调用,即catch子句的内容,也像普通方法的调用一样被编译。然而,catch子句的存在导致编译器生成一个异常表项(§2.10, §4.7.3)。catchOne方法的异常表有一个条目,对应于catchOne的catch子句可以处理的一个参数(TestExc类的一个实例)。if在执行catchOne的索引0到4之间的指令时,抛出了一些TestExc的实例值,那么控制权将被转移到索引5的Java虚拟机代码,该代码实现了catch子句的块。if被抛出的值不是TestExc的实例,catchOne的catch子句不能处理它。相反,该值被重新抛出到catchOne的调用者那里。
一个try可以有多个catch条款。
void catchTwo() {
try {
tryItOut();
} catch (TestExc1 e) {
handleExc(e);
} catch (TestExc2 e) {
handleExc(e);
}
}
一个给定的try语句的多个catch子句在编译时,只需将每个catch子句的Java虚拟机代码一个接一个地追加,并在异常表中添加条目,如图所示。
方法 void catchTwo()
0 aload_0 // 开始尝试块
1 invokevirtual #5 // Method Example.tryItOut()V
4 return // 尝试块的结束;正常返回
5 astore_1 // TestExc1的处理程序的开始。
//将抛出的值存储在本地var 1中
6 aload_0 // 推动这个
7 aload_1 // 推送抛出值
8 invokevirtual #7 // 调用处理程序方法。
// Example.handleExc(LTestExc1;)V
11 return // 处理完TestExc1后返回
12 astore_1 // TestExc2的处理程序的开始。
//将抛出的值存储在本地var 1中
13 aload_0 // 推动这个
14 aload_1 // 推送抛出值
15 invokevirtual #7 // 调用处理程序方法。
// Example.handleExc(LTestExc2;)V
18 return // 处理完TestExc2后返回
异常表。
从到目标类型
0 4 5类TestExc1
0 4 12类TestExc2
if在try子句的执行过程中(在索引0和4之间)抛出一个与一个或多个catch子句的参数相匹配的值(该值是一个或多个参数的实例),则选择第一个(最里面的)此类catch子句。控制权被转移到该catch子句块的Java虚拟机代码中。if抛出的值与catchTwo的任何一个catch子句的参数不匹配,Java虚拟机就会重新抛出该值,而不调用catchTwo的任何一个catch子句中的代码。
嵌套的try-catch语句的编译方式非常像带有多个catch子句的try语句。
void nestedCatch() {
try {
try {
tryItOut();
} catch (TestExc1 e) {
handleExc1(e);
}
} catch (TestExc2 e) {
handleExc2(e);
}
}
成为。
方法 void nestedCatch()
0 aload_0 // 开始尝试块
1 invokevirtual #8 // Method Example.tryItOut()V
4 return // 尝试块的结束;正常返回
5 astore_1 // TestExc1的处理程序的开始。
//将抛出的值存储在本地var 1中
6 aload_0 // 推动这个
7 aload_1 // 推送抛出值
8 invokevirtual #7 // 调用处理程序方法。
// Example.handleExc1(LTestExc1;)V
11 return // 处理完TestExc1后返回
12 astore_1 // TestExc2的处理程序的开始。
//将抛出的值存储在本地var 1中
13 aload_0 // 推动这个
14 aload_1 // 推送抛出值
15 invokevirtual #6 // 调用处理程序方法。
// Example.handleExc2(LTestExc2;)V
18 return // 处理完TestExc2后返回
异常表。
从到目标类型
0 4 5类TestExc1
0 12 12类TestExc2
捕获子句的嵌套只在异常表中表示。Java 虚拟机并不强制执行异常表项的嵌套或任何排序(§2.10)。然而,由于 try-catch 构造是结构化的,编译器总是可以对异常处理表的条目进行排序,这样,对于任何被抛出的异常和该方法中的任何程序计数器值,与被抛出的异常相匹配的第一个异常处理程序对应于最内层的匹配 catch 子句。
例如,iftryItOut的调用(在索引1)抛出了TestExc1的实例,它将被调用handleExc1的catch子句处理。即使该异常发生在外部捕获子句(捕获TestExc2)的范围内,即使该外部捕获子句可能已经能够处理该抛出的值,也是如此。
作为一个微妙的观点,注意catch子句的范围在 "from "端是包容的,在 "to "端是排他的(§4.7.3)。因此,捕获TestExc1的catch子句的异常表项并不包括偏移量4的返回指令。然而,捕获TestExc2的catch子句的异常表项包括偏移量为11的返回指令。嵌套的catch子句中的返回指令包括在嵌套的catch子句所涵盖的指令范围内。
3.13.最后编译
(本节假设编译器生成的类文件的版本号为50.0或以下,这样就可以使用jsr指令。参见§4.10.2.5)。
try-finally语句的编译与try-catch的编译类似。在把控制权转移到try语句之外之前,不管这种转移是正常的还是突然的,因为已经抛出了一个异常,必须首先执行final子句。对于这个简单的例子。
void tryFinally() {
try {
tryItOut();
} finally {
wrapItUp();
}
}
编译后的代码是。
方法 void tryFinally()
0 aload_0 // 尝试块的开始
1 invokevirtual #6 // Method Example.tryItOut()V
4 jsr 14 // 调用最终块
7 return // 尝试块的结束
8 astore_1 // 任何抛出的处理程序的开始。
9 jsr 14 // Call finally block
12 aload_1 // 推送抛出值
13 athrow // ...并向调用者重新抛出值
14 astore_2 // 最终块的开始
15 aload_0 // 推动这个
16 invokevirtual #5 // Method Example.wrapItUp()V
19 ret 2 // 从最终块返回
异常表。
从到目标类型
0 4 8 任何
有四种方法可以使控制权在try语句之外转移:通过落入该块的底部,通过返回,通过执行break或continue语句,或者通过引发一个异常。iftryItOut返回而没有引发异常,控制权就会通过jsr指令转移到final块。索引4的jsr 14指令对索引14的final块的代码进行 “子程序调用”(final块被编译为一个嵌入式子程序)。当最终块完成后,ret 2指令将控制权返回到索引4的jsr指令之后的指令。
更详细地说,子程序调用的工作原理如下。jsr指令在跳转前将下面指令的地址(索引7处的return)推到操作栈中。作为跳转目标的store_2指令将操作栈上的地址存入局部变量2中。最后区块的代码(在这里是load_0和invokevirtual指令)被运行。假设该代码的执行正常完成,ret指令从局部变量2中获取地址,并在该地址继续执行。return指令被执行,tryFinally正常返回。
一个带有finally子句的try语句被编译为有一个特殊的异常处理程序,这个异常处理程序可以处理在try语句中抛出的任何异常。if tryItOut 抛出了一个异常,那么 tryFinally 的异常表会被搜索到一个合适的异常处理程序。这个特殊的处理程序被找到,导致执行在索引8处继续。索引8处的store_1指令将抛出的值存入本地变量1。下面的jsr指令对最终块的代码做了一个子程序调用。假设代码正常返回,索引12处的load_1指令将抛出的值推回操作数堆栈,下面的athrow指令重新抛出该值。
编译一个既有catch子句又有final子句的try语句就比较复杂。
void tryCatchFinally() {
try {
tryItOut();
} catch (TestExc e) {
handleExc(e);
} finally {
wrapItUp();
}
}
成为。
方法 void tryCatchFinally()
0 aload_0 // 尝试块的开始
1 invokevirtual #4 // Method Example.tryItOut()V
4 goto 16 // 跳转到最终块
7 astore_3 // TestExc的处理程序的开始。
//将抛出的值存储在本地var 3中
8 aload_0 // 推动这个
9 aload_3 // 推送抛出值
10 invokevirtual #6 // 调用处理程序方法。
// Example.handleExc(LTestExc;)V
13 goto 16 // 这个goto是没有必要的,但也是有必要的。
//由JDK 1.0.2中的javac生成
16 jsr 26 // 调用最终块
19 return // 处理完TestExc后返回
20 astore_1 // 异常处理程序的起始部分
//除TestExc外,其他都是例外
// 在处理TestExc时被抛出
21 jsr 26 // 调用最终块
24 aload_1 // 推送抛出的值...
25 athrow // ...并向调用者重新抛出值
26 astore_2 // 最后块的开始
27 aload_0 // 推动这个
28 invokevirtual #5 // Method Example.wrapItUp()V
31 ret 2 // 从最终块返回
异常表。
从到目标类型
0 4 7类TestExc
0 16 20 任何
iftry语句正常完成,索引4的goto指令跳转到索引16的final块的子程序调用。索引26的final块被执行,控制权返回到索引19的return指令,tryCatchFinally正常返回。
if tryItOut 抛出一个 TestExc 的实例,那么异常表中第一个(最里面的)适用的异常处理程序被选择来处理这个异常。该异常处理程序的代码,从索引7开始,将抛出的值传递给handleExc,并在其返回时对索引26的final块进行相同的子程序调用,就像在正常情况下一样。ifhandleExc没有抛出异常,tryCatchFinally会正常返回。
iftryItOut抛出的值不是TestExc的实例,或者handleExc本身抛出了一个异常,那么这个条件就由异常表的第二个条目来处理,它处理索引0到16之间抛出的任何值。该异常处理程序将控制权转移到索引20,在那里抛出的值首先被存储在局部变量1中。索引26处的最终块的代码作为一个子程序被调用。if它返回,抛出的值就会从局部变量1中检索出来,并使用atrow指令重新抛出。if在执行finally子句的过程中抛出了一个新的值,finally子句就会中止,tryCatchFinally就会突然返回,把新的值抛给它的调用者。
3.14.同步化
Java虚拟机中的同步是通过监视器的进入和退出来实现的,可以是显式的(通过使用monitorenter和monitorexit指令),也可以是隐式的(通过方法调用和返回指令)。
对于用Java编程语言编写的代码,最常见的同步形式可能是同步方法。一个同步方法通常不使用monitorenter和monitorexit来实现。相反,它只是在运行时常量池中被ACC_SYNCHRONIZED标志所区分,该标志由方法调用指令检查(§2.11.10)。
monitorenter和monitorexit指令可以实现同步语句的编译。比如说。
void onlyMe(Foo f) {
synchronized(f) {
doSomething()。
}
}
被编译为。
方法 void onlyMe(Foo)
0 aload_1 // 推动f
1 dup // 在栈上复制它。
2 astore_2 // 在本地变量中存储重复的内容 2
3 monitorenter // 输入与f相关的显示器。
4 aload_0 // 保持显示器,通过这个和...
5 invokevirtual #5 // ...调用 Example.doSomething()V
8 aload_2 // 推送局部变量2 (f)
9 monitorexit // 退出与f相关的监视器。
10 goto 18 // 正常完成该方法
13 astore_3 // if有任何投掷,在这里结束。
14 aload_2 // 推送局部变量2 (f)
15 monitorexit // 一定要退出监视器!
16 aload_3 // 推送抛出的值...
17 athrow // ...并向调用者重新抛出值
18 return // 在正常情况下返回
异常表。
从到目标类型
4 10 13 任何
13 16 13 任何
编译器确保在任何方法调用完成时,对于自方法调用以来执行的每一条monitorenter指令都会执行一条monitorexit指令。无论方法调用是正常完成(§2.6.4)还是突然完成(§2.6.5),情况都是如此。为了在突然的方法调用完成时强制执行monitorenter和monitorexit指令的正确配对,编译器生成了异常处理程序(§2.10),它将匹配任何异常,其相关代码将执行必要的monitorexit指令。
3.15.注释
类文件中注释的表示方法在§4.7.16-§4.7.22中描述。这些章节明确了如何表示对类、接口、字段、方法、方法参数和类型参数的声明的注释,以及对这些声明中使用的类型的注释。对包声明的注释需要额外的规则,在此给出。
当编译器遇到一个必须在运行时提供的注解包声明时,它会发出一个具有以下属性的类文件。
· 类文件代表了一个接口,也就是说,ClassFile结构的ACC_INTERFACE和ACC_ABSTRACT标志被设置(§4.1)。
· if类文件的版本号小于50.0,那么ACC_SYNTHETIC标志就不设置;if类文件的版本号是50.0或以上,那么ACC_SYNTHETIC标志就设置。
· 该接口具有包的访问权限(JLS §6.6.1)。
· 该接口的名称是package-name.package-info的内部形式(§4.2.1)。
· 该接口没有超接口。
· 该接口的唯一成员是《Java语言规范,Java SE 8版》(JLS §9.2)所暗示的成员。
· 包声明上的注释被作为RuntimeVisibleAnnotations和RuntimeInvisibleAnnotations属性存储在ClassFile结构的属性表中。
第四章 类文件格式
本章介绍了Java虚拟机的类文件格式。每个类文件都包含一个单一的类或接口的定义。尽管一个类或接口不需要有一个字面上包含在文件中的外部表示(例如,因为该类是由一个类加载器生成的),我们将俗称一个类或接口的任何有效表示为类文件格式。
一个类文件由一个8位字节流组成。所有16位、32位和64位的数量分别通过读入两个、四个和八个连续的8位字节来构建。多字节的数据项总是以big-endian的顺序存储,其中高字节在前。在Java SE平台上,这种格式由接口java.io.DataInput和java.io.DataOutput以及java.io.DataInputStream和java.io.DataOutputStream等类支持。
本章定义了自己的一组数据类型,代表类文件数据。u1、u2和u4类型分别代表一个无符号的1、2或4字节的数量。在Java SE平台上,这些类型可以通过java.io.DataInput接口的readUnsignedByte、readUnsignedShort和readInt等方法读取。
本章用类似C语言的结构符号编写的伪结构来介绍类文件格式。为了避免与类和类实例等的字段相混淆,描述类文件格式的结构的内容被称为项。连续的项是按顺序存储在类文件中的,没有填充或对齐。
表由零个或多个可变大小的项目组成,在一些类文件结构中使用。尽管我们使用类似C语言的数组语法来指代表项,但表是大小不一的结构流,这意味着不可能将表的索引直接转化为表内的字节偏移。
当我们把一个数据结构称为数组时,它由零个或多个连续的固定大小的项目组成,可以像数组一样进行索引。
本章中提到的ASCII字符应被解释为与ASCII字符相对应的Unicode码位。
4.1.类文件结构
一个类文件由一个单一的ClassFile结构组成。
类文件 {
u4魔法。
u2 minor_version。
u2 major_version。
u2 constant_pool_count。
cp_info constant_pool[constant_pool_count-1]。
u2 access_flags。
u2 this_class;
u2 super_class;
u2接口_count。
u2接口[interface_count]。
u2 fields_count。
field_info fields[field_count]。
u2 methods_count。
method_info方法[methods_count]。
u2 attributes_count。
attribute_info属性[attribute_count]。
}
ClassFile结构中的项目如下。
magic项提供了识别类文件格式的魔法号码;它的值是0xCAFEBABE。
minor_version, major_version
minor_version和major_version项的值是这个类文件的次要和主要版本号。主版本号和次版本号共同决定了类文件格式的版本。if一个类文件的主要版本号是M,次要版本号是m,我们就把它的类文件格式的版本表示为M.m。因此,类文件格式的版本可以按词法排序,例如,1.5 < 2.0 < 2.1。
一个Java虚拟机实现可以支持版本v的类文件格式,当且仅当v位于某个连续的范围Mi.0≤v≤Mj.m。
JDK 1.0.2 版中的 Oracle Java 虚拟机实现支持 45.0 到 45.3(含)的类文件格式版本。JDK 1.1. 版支持 45.0 至 45.65535(含)范围内的类文件格式版本。对于 k ≥ 2,JDK 1.k 版支持class文件格式版本在 45.0 到 44+k.0(含)范围内。
constant_pool_count
constant_pool_count项的值等于 constant_pool表中的条目数加1。if一个 constant_pool索引大于零且小于 constant_pool_count,则被认为是有效的,§4.4.5 中提到的 long和 double类型的常数除外。
constant_pool[]
constant_pool是一个结构表 (§4.4),代表各种字符串常量、类和接口名称、字段名称,以及其他在 ClassFile结构及其子结构中被引用的常量。每个 constant_pool表项的格式由其第一个 "标签 "字节表示。
constant_pool表的索引范围是1到constant_pool_count- 1。
访问_flags
access_flags项的值是一个标志的掩码,用于表示对这个类或接口的访问权限和属性。表4.1-A中规定了每个标志在设置时的解释。
表4.1-A类的访问和属性修改器
| 名称 | 值 | 解释 |
|---|---|---|
ACC_PUBLIC | 0x0001 | 宣布为公共的;可以从它的包之外访问。 |
ACC_FINAL | 0x0010 | 被宣布为最终的;不允许有子类。 |
ACC_SUPER | 0x0020 | 当被invokespecial指令调用时,特别对待超类方法。 |
ACC_INTERFACE | 0x0200 | 是一个接口,而不是一个类。 |
ACC_ABSTRACT | 0x0400 | 宣布为抽象的;不能被实例化。 |
ACC_SYNTHETIC | 0x1000 | 宣称是合成的;在源代码中不存在。 |
ACC_ANNOTATION | 0x2000 | 作为一个注解类型被宣布。 |
ACC_ENUM | 0x4000 | 作为一个枚举类型被宣布。 |
一个接口是通过设置ACC_INTERFACE标志来区分的。ifACC_INTERFACE标志没有被设置,这个类文件定义了一个类,而不是一个接口。
if设置了ACC_INTERFACE标志,还必须设置ACC_ABSTRACT标志,并且不能设置ACC_FINAL、ACC_SUPER和ACC_ENUM标志。
if没有设置ACC_INTERFACE标志,除了ACC_ANNOTATION之外,表4.1-A中的任何其他标志都可以被设置。然而,这样的类文件不能同时设置ACC_FINAL和ACC_ABSTRACT标志(JLS §8.1.1.2)。
ACC_SUPER标志表明,ifinvokespecial指令(§invokespecial)出现在这个类或接口中,它将表达两种替代语义中的哪一种。Java虚拟机指令集的编译器应该设置ACC_SUPER标志。在Java SE 8及以上版本中,Java虚拟机认为每个类文件中都设置了ACC_SUPER标志,而不管类文件中该标志的实际值和类文件的版本。
ACC_SUPER标志的存在是为了向后兼容由 Java 编程语言的旧编译器编译的代码。在1.0.2之前的JDK版本中,编译器生成的access_flags中,现在代表ACC_SUPER的标志没有指定的含义,if该标志被设置,Oracle的Java虚拟机实现会忽略它。
ACC_SYNTHETIC标志表示这个类或接口是由编译器生成的,不在源代码中出现。
一个注解类型必须设置其ACC_ANNOTATION标志。ifACC_ANNOTATION标志被设置,ACC_INTERFACE标志也必须被设置。
ACC_ENUM标志表示该类或其超类被声明为一个枚举类型。
表4.1-A中未分配的access_flags项的所有位都保留给未来使用。它们应该在生成的类文件中被设置为零,并且应该被Java虚拟机的实现所忽略。
this_class
this_class项的值必须是 constant_pool表中的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Class_info结构(§4.4.1),代表这个类文件所定义的类或接口。
super类
对于一个类,super_class项的值必须为零,或者必须是constant_pool表中的一个有效索引。ifsuper_class项的值为非零,那么该索引中的constant_pool条目必须是一个CONSTANT_Class_info结构,代表这个类文件所定义的类的直接超类。直接超类和它的任何超类都不能在其ClassFile结构的access_flags项中设置ACC_FINAL标志。
ifsuper_class项的值为0,那么这个类文件必须代表Object类,这是唯一没有直接超类的类或接口。
对于一个接口,super_class项的值必须始终是constant_pool表中的一个有效索引。该索引中的constant_pool条目必须是一个代表Object类的CONSTANT_Class_info结构。
接口_count
interfaces_count项的值给出了这个类或接口类型的直接超接口的数量。
接口[]
interfaces数组中的每个值必须是constant_pool表中的一个有效索引。在interface[i]的每个值上的constant_pool条目,其中0≤i < interfaces_count,必须是一个CONSTANT_Class_info结构,代表该类或接口类型的直接超接口,按照类型源中给出的从左到右的顺序。
字段_count
fields_count项的值给出了字段表中的field_info结构的数量。field_info结构代表所有的字段,包括类变量和实例变量,由这个类或接口类型声明。
字段[]
字段表中的每个值必须是一个field_info结构(§4.5),给出这个类或接口中一个字段的完整描述。字段表只包括那些由这个类或接口声明的字段。它不包括代表从超类或超接口继承的字段的项目。
Method_count
methods_count项的值给出了方法表中method_info结构的数量。
方法[]
方法表中的每个值都必须是一个 method_info结构(§4.6),对这个类或接口中的一个方法进行完整的描述。ifACC_NATIVE和ACC_ABSTRACT这两个标志都没有在method_info结构的access_flags项中设置,那么也会提供实现该方法的Java虚拟机指令。
method_info结构表示这个类或接口类型声明的所有方法,包括实例方法、类方法、实例初始化方法(§2.9)以及任何类或接口初始化方法(§2.9)。方法表不包括代表从超类或超接口继承的方法的项目。
属性_count
attributes_count项的值给出了这个类的属性表中的属性数量。
属性[]
属性表的每个值必须是一个attribute_info结构(§4.7)。
表4.7-C中列出了本规范所定义的出现在ClassFile结构的属性表中的各种属性。
有关定义在 ClassFile结构的属性表中出现的属性的规则在 §4.7 中给出。
关于 ClassFile结构的属性表中的非预定义属性的规则在 §4.7.1 中给出。
4.2.名称的内部形式
4.2.1.二进制类和接口名称
出现在类文件结构中的类和接口名称总是以完全限定的形式表示,称为二进制名称(JLS §13.1)。这些名字总是以CONSTANT_Utf8_info结构表示(§4.4.7),因此可以从整个Unicode代码空间中提取,if没有进一步的限制。类和接口名称可从那些将此类名称作为其描述符(§4.3)一部分的CONSTANT_NameAndType_info结构(§4.4.6)以及所有CONSTANT_Class_info结构(§4.4.1)中引用。
由于历史原因,出现在类文件结构中的二进制名称的语法与JLS §13.1中记载的二进制名称的语法不同。在这种内部形式中,通常分隔构成二进制名称的标识符的ASCII句号(.)被ASCII正斜线(/)所取代。标识符本身必须是不合格的名称(§4.2.2)。
例如,Thread类的正常二进制名称是java.lang.Thread。在类文件格式的描述符中使用的内部形式中,对类Thread名称的引用是通过一个代表字符串java/lang/Thread的CONSTANT_Utf8_info结构实现的。
4.2.2.不合格的名称
方法、字段、局部变量和形式参数的名称被存储为非限定的名称。一个不合格的名字必须至少包含一个Unicode代码点,并且不能包含任何ASCII字符. ; [ /(即句号或分号或左方括号或正斜杠)。
方法名称被进一步限制,除了特殊的方法名称<init>和<clinit>(§2.9)外,它们不能包含ASCII字符<或>(即左角括号或右角括号)。
注意,字段名或接口方法名可以是<init>或<clinit>,但任何方法调用指令都不能引用<clinit>,只有 invokespecial指令(§\invokespecial)可以引用<init>。
4.3.描述符
描述符是一个字符串,代表一个字段或方法的类型。描述符在类文件格式中使用修改过的UTF-8字符串表示(§4.4.7),因此在没有进一步限制的情况下,可以从整个Unicode编码空间中提取。
4.3.1.语法记号
描述符是用一个语法指定的。语法是一组描述字符序列如何形成各种语法正确的描述符的产物。语法的终端符号以固定宽度字体显示。非终端符号以斜体字显示。非终端的定义由被定义的非终端的名称引入,后面有一个冒号。然后在后面的行中有一个或多个非终端的替代定义。
语法{x}在生产的右侧表示x的零或多个出现。
产地右侧的短语(之一)标志着下面一行或几行的每个终端符号都是备选定义。
4.3.2.字段描述符
一个字段描述符代表了一个类、实例或局部变量的类型。
FieldDescriptor。
字段类型。
基础类型
(其中之一)
B C D F I J S Z
对象类型。
L`类名`;
阵列类型。
组件类型
BaseType的字符、ObjectType的和以及ArrayType的都是ASCII字符。
ClassName代表一个以内部形式编码的二进制类或接口名称(§4.2.1)。
场描述符作为类型的解释见表4.3-A。
代表一个数组类型的字段描述符只有在代表一个具有255个或更少维度的类型时才有效。
表4.3-A.现场描述符的解释
| 字段类型术语 | 类型 | 解释 |
|---|---|---|
B | byte | 有符号的字节 |
C | character | 基本多语言平面中的Unicode字符码位,用UTF-16编码。 |
D | double | 双精度浮点值 |
F | `float | 单精度浮点值 |
I | integer | 整数 |
J | Long | 长整数 |
L类名; | object | 一个ClassName的实例 |
S | short | 签名的短文 |
Z | Boolean | 真或假 |
[ | 数组 | 一个数组维度 |
int类型的实例变量的字段描述符是简单的I。
Object类型的实例变量的字段描述符是Ljava/lang/Object;。注意,使用的是Object类的二进制名称的内部形式。
多维数组类型double[][][]的实例变量的字段描述符是[[[D。
4.3.3.方法描述符
一个方法描述符包含零个或多个参数描述符,代表该方法接受的参数类型,以及一个返回描述符,代表该方法返回值的类型(if有的话)。
方法描述符。
({ParameterDescriptor} )ReturnDescriptor
ParameterDescriptor。
ReturnDescriptor。
VoidDescriptor。
V
字符V表示该方法不返回任何值(其结果为无效)。
该方法的方法描述符。
object m(int i,double d,Thread t){...}
如下
(IDLjava/lang/Thread;)Ljava/lang/Object;
请注意,使用的是Thread和Object的二进制名称的内部形式。
一个方法描述符只有在代表总长度为255或更少的方法参数时才是有效的,在实例或接口方法调用的情况下,这个长度包括对它的贡献。总长度的计算方法是将各个参数的贡献相加,其中long或double类型的参数贡献两个单位的长度,其他类型的参数贡献一个单位。
无论它描述的方法是一个类方法还是一个实例方法,方法描述符都是一样的。尽管一个实例方法会传递这个,即除了它的预期参数外,还传递给被调用的对象的引用,但这个事实并没有反映在方法描述符中。对这个的引用是由调用实例方法的Java虚拟机指令隐式传递的(§2.6.1,§4.11)。
4.4.常量池
Java虚拟机指令不依赖于类、接口、类实例或数组的运行时布局。相反,指令参考了constant_pool表中的符号信息。
所有constant_pool表项的一般格式如下。
cp_info {
u1标签。
u1信息[]。
}
constant_pool表中的每一项都必须以一个1字节的标签开始,表明cp_info条目的种类。信息数组的内容随着标签值的不同而变化。表4.4-A中列出了有效的标签和它们的值。每个标签字节后面必须有两个或更多的字节,提供关于特定常量的信息。附加信息的格式随标签值的变化而变化。
表4.4-A.恒定池标签
| 恒定类型 | 价值 |
|---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_LONG | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
4.4.1.CONSTANT_Class_info结构
CONSTANT_Class_info结构用于表示一个类或一个接口。
CONSTANT_Class_info {
u1标签。
u2 name_index。
}
CONSTANT_Class_info结构的项目如下。
标签
该标签项的值为CONSTANT_Class(7)。
名称_索引
name_index项的值必须是constant_pool表中的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表一个以内部形式编码的有效二进制类或接口名称(§4.2.1)。
因为数组是对象,操作码anewarray和multianewarray–但不是操作码new–可以通过constant_pool表中的CONSTANT_Class_info结构引用数组 “类”。对于这样的数组类,该类的名称是数组类型的描述符(§4.3.2)。
例如,代表二维数组类型int[][]的类名是[[I,而代表Thread[]类型的类名是[Ljava/lang/Thread;。
一个数组类型描述符只有在代表255个或更少维度时才有效。
4.4.2.CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info结构
字段、方法和接口方法都由类似的结构表示。
CONSTANT_Fieldref_info {
u1标签。
u2 class_index;
u2 name_and_type_index。
}
CONSTANT_Methodref_info {
u1标签。
u2 class_index;
u2 name_and_type_index。
}
CONSTANT_InterfaceMethodref_info {
u1标签。
u2 class_index;
u2 name_and_type_index。
}
这些结构的项目如下。
标签
CONSTANT_Fieldref_info结构的标签项具有CONSTANT_Fieldref(9)的值。
CONSTANT_Methodref_info结构的标签项具有CONSTANT_Methodref(10)的值。
CONSTANT_InterfaceMethodref_info结构的标签项具有CONSTANT_InterfaceMethodref(11)的值。
类的索引
class_index项的值必须是constant_pool表中的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Class_info结构(§4.4.1),代表一个以该字段或方法为成员的类或接口类型。
CONSTANT_Methodref_info结构中的class_index项必须是一个类的类型,而不是一个接口类型。
CONSTANT_InterfaceMethodref_info结构中的class_index项必须是一个接口类型。
CONSTANT_Fieldref_info结构中的class_index项可以是一个类的类型,也可以是一个接口类型。
名称和类型索引
name_and_type_index项的值必须是 constant_pool表的一个有效索引。该索引中的 constant_pool条目必须是一个 CONSTANT_NameAndType_info结构(§4.4.6)。这个 constant_pool条目表示字段或方法的名称和描述符。
在CONSTANT_Fieldref_info中,指定的描述符必须是一个字段描述符(§4.3.2)。否则,指示的描述符必须是一个方法描述符(§4.3.3)。
ifCONSTANT_Methodref_info结构中的方法名称以'<’ (’\u003c’)开头,那么该名称必须是特殊名称<init>,代表一个实例初始化方法(§2.9)。这种方法的返回类型必须是void。
4.4.3.CONSTANT_String_info结构
CONSTANT_String_info结构用于表示String类型的常量对象。
CONSTANT_String_info {
u1标签。
u2 string_index。
}
CONSTANT_String_info结构的项目如下。
标签
CONSTANT_String_info结构的标签项的值是CONSTANT_String(8)。
字符串_索引
string_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表String对象要被初始化的Unicode码点序列。
4.4.4.CONSTANT_Integer_info和CONSTANT_Float_info结构
CONSTANT_Integer_info和CONSTANT_Float_info结构表示4字节的数字(int和float)常量。
CONSTANT_Integer_info {
u1标签。
u4字节。
}
CONSTANT_Float_info {
u1标签。
u4字节。
}
这些结构的项目如下。
标签
CONSTANT_Integer_info结构的标签项的值是CONSTANT_Integer(3)。
CONSTANT_Float_info结构的标签项的值是CONSTANT_Float(4)。
字节
CONSTANT_Integer_info结构的字节项表示int常数的值。该值的字节以big-endian(高字节优先)的顺序存储。
CONSTANT_Float_info结构的字节项表示IEEE 754浮点单格式的浮点常数的值(§2.3.2)。单一格式表示法的字节以big-endian(高字节优先)的顺序存储。
CONSTANT_Float_info结构所代表的值是按以下方式确定的。首先将该值的字节转换为int常数位。然后。
· if比特是0x7f800000,浮动值将是正无穷大。
· if比特是0xff800000,浮动值将是负无穷。
· if比特在0x7f800001到0x7fffffff的范围内,或者在0xff800001到0xffffffff的范围内,浮点数将是NaN。
· 在所有其他情况下,让s、e和m是三个可能由比特计算出来的值。
· int s = ((bits >> 31) == 0) ?1 : -1;
· int e = ((bits >> 23) & 0xff);
· int m = (e == 0) ?
· (bits & 0x7fffff) << 1 :
· (bits & 0x7fffff) | 0x800000。
那么浮动值等于数学表达式s-m-2的结果e-150 。
4.4.5.CONSTANT_Long_info和CONSTANT_Double_info结构
CONSTANT_Long_info和CONSTANT_Double_info表示8字节的数字(长和双)常量。
CONSTANT_Long_info {
u1标签。
u4 high_bytes。
u4 low_bytes。
}
CONSTANT_Double_info {
u1标签。
u4 high_bytes。
u4 low_bytes。
}
所有8字节的常量在类文件的constant_pool表中占了两个条目。ifCONSTANT_Long_info或CONSTANT_Double_info结构是constant_pool表中索引n处的项目,那么池中下一个可用的项目就位于索引n+2处。constant_pool索引n+1必须是有效的,但被认为是不可用的。
现在回想起来,让8字节的常量占用两个常量池条目是一个糟糕的选择。
这些结构的项目如下。
标签
CONSTANT_Long_info结构的标签项的值是CONSTANT_Long(5)。
CONSTANT_Double_info结构的标签项的值是CONSTANT_Double(6)。
high_bytes, low_bytes
CONSTANT_Long_info结构中无符号的high_bytes和low_bytes项共同代表长常数的值。
((long) high_bytes << 32) + low_bytes
其中high_bytes和low_bytes中的每一个字节都是按照big-endian(高字节优先)的顺序存储的。
CONSTANT_Double_info结构的high_bytes和low_bytes项共同代表了IEEE 754浮点双倍格式的双倍值(§2.3.2)。每个项目的字节都是按照big-endian(高字节在前)的顺序存储。
CONSTANT_Double_info结构所代表的值确定如下。high_bytes和low_bytes项被转换为长常数位,等于
((long) high_bytes << 32) + low_bytes
然后。
· if比特是0x7ff0000000000000L,双倍值将是正无穷大。
· if比特是0xfff0000000000000L,双倍值将是负无穷。
· if比特在0x7ff0000000000001L到0x7fffffffffffL的范围内,或者在0xfff0000000000001L到0xffffffffffL的范围内,双倍值将是NaN。
· 在所有其他情况下,让s、e和m是三个可能由比特计算出来的值。
· int s = ((bits >> 63) == 0) ?1 : -1;
· int e = (int)((bits >> 52) & 0x7ffL)。
· long m = (e == 0) ?
· (bits & 0xfffffffffL) << 1 :
· (bits & 0xfffffffffffL) | 0x10000000000000L。
那么浮点值就等于数学表达式s - m - 2的双倍值e-1075 。
4.4.6.CONSTANT_NameAndType_info结构
CONSTANT_NameAndType_info结构用于表示一个字段或方法,而不表明它属于哪个类或接口类型。
CONSTANT_NameAndType_info {
u1标签。
u2 name_index。
u2 descriptor_index。
}
CONSTANT_NameAndType_info结构的项目如下。
标签
CONSTANT_NameAndType_info结构的标签项的值是CONSTANT_NameAndType(12)。
名称_索引
name_index项的值必须是 constant_pool表的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表特殊方法名称<init>(§2.9)或表示字段或方法的有效非限定名称(§4.2.2)。
描述符_索引
descriptor_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表一个有效的字段描述符或方法描述符(§4.3.2, §4.3.3)。
4.4.7.CONSTANT_Utf8_info结构
CONSTANT_Utf8_info结构用于表示常量字符串值。
CONSTANT_Utf8_info {
u1标签。
u2长度。
u1 bytes[length]。
}
CONSTANT_Utf8_info结构的项目如下。
标签
CONSTANT_Utf8_info结构的标签项的值是CONSTANT_Utf8(1)。
长度
length项的值给出字节数组中的字节数(而不是生成的字符串的长度)。
字节[]
字节数组包含字符串的字节数。
任何字节都不能有(byte)0的值。
任何字节都不能在(byte)0xf0到(byte)0xff范围内。
字符串内容是用修改过的UTF-8编码的。修改后的UTF-8字符串的编码方式是,只包含非空ASCII字符的码点序列可以用每个码点的1个字节来表示,但Unicode编码空间的所有码点都可以被表示。修改后的UTF-8字符串是没有空尾的。编码方式如下。
· 在’\u0001‘到’\u007F'范围内的代码点由一个字节表示。
表4.4.
| 0 | 第6-0位 |
|---|---|
字节中的7位数据给出了所代表的码位的值。
· 空码位(’\u0000’)和’`u0080'到'\u07FF'范围内的码位由一对字节x和y`表示。
表4.5.
x: | 表4.6. 1 1 0 位数 10-6 |
|---|---|
y: | 表4.7. 1 0 第5-0位 |
这两个字节代表了带有数值的代码点。
((x & 0x1f) << 6) + (y & 0x3f)
· 在’\u0800‘到’\uFFFF'范围内的代码点由3个字节x、y和z表示。
表4.8.
x: | 表4.9. 1 1 1 0 第15-12位 |
|---|---|
y: | 表4.10. 1 0 第11-6位 |
z: | 表4.11. 1 0 第5-0位 |
这三个字节代表了带有数值的代码点。
((x & 0xf) << 12) + ((y & 0x3f) << 6) + (z & 0x3f)
· 代码点在U+FFFF以上的字符(所谓的补充字符)是通过对其UTF-16表示的两个代理代码单元单独编码来表示的。每个代理代码单元由三个字节表示。这意味着补充字符由六个字节表示,即u、v、w、x、y和z。
表4.12.
u: | 表4.13. 1 1 1 0 1 1 0 1 |
|---|---|
v: | 表4.14. 1 0 1 0 (第20-16位)-1 |
w: | 表4.15. 1 0 第15-10位 |
x: | 表4.16. 1 1 1 0 1 1 0 1 |
y: | 表4.17. 1 0 1 1 第9-6位 |
z: | 表4.18. 1 0 第5-0位 |
这六个字节代表了带有数值的代码点。
0x10000 + ((v & 0x0f) << 16) + ((w & 0x3f) << 10) +
((y & 0x0f) << 6) + (z & 0x3f)
多字节字符的字节以big-endian(高字节优先)的顺序存储在类文件中。
这种格式与 "标准 "UTF-8格式有两个区别。首先,空字符(char)0使用2字节格式而不是1字节格式进行编码,因此修改后的UTF-8字符串永远不会有嵌入式空字符。第二,只使用标准UTF-8的1字节、2字节和3字节的格式。Java虚拟机不能识别标准UTF-8的4字节格式;它使用自己的2-3字节格式。
关于标准UTF-8格式的更多信息,请参见\《统一码标准》第6.0.0版第3.9节\统一码编码形式\。
4.4.8.CONSTANT_MethodHandle_info结构
CONSTANT_MethodHandle_info结构用于表示一个方法句柄。
CONSTANT_MethodHandle_info {
u1标签。
u1 reference_kind。
u2 reference_index。
}
CONSTANT_MethodHandle_info结构的项目如下。
标签
CONSTANT_MethodHandle_info结构的标签项的值是CONSTANT_MethodHandle(15)。
参考类型
reference_kind项的值必须在1到9之间。这个值表示这个方法句柄的种类,它表征了它的字节码行为(§5.4.3.5)。
参考文献_索引
reference_index项的值必须是 constant_pool表中的一个有效索引。该索引中的 constant_pool条目必须如下。
· if reference_kind项的值是 1 (REF_getField), 2 (REF_getStatic), 3 (REF_putField), 或 4 (REF_putStatic), 那么该索引的 constant_pool项必须是一个 CONSTANT_Fieldref_info(§4.4.2) 结构,代表一个要创建方法柄的字段。
· if reference_kind项的值是 5 (REF_invokeVirtual) 或 8 (REF_newInvokeSpecial),那么该索引的 constant_pool条目必须是一个 CONSTANT_Methodref_info结构(§4.4.2),代表一个类的方法或构造函数(§2.9),为其创建一个方法句柄。
· if reference_kind项的值是 6 (REF_invokeStatic) 或 7 (REF_invokeSpecial),那么if类文件的版本号小于 52.0,该索引处的 constant_pool条目必须是一个 CONSTANT_Methodref_info结构,代表一个要创建方法句柄的类的方法;if类文件版本号是 52.if类文件的版本号是52.0或以上,该索引处的constant_pool条目必须是CONSTANT_Methodref_info结构或CONSTANT_InterfaceMethodref_info结构(§4.4.2),代表一个类或接口的方法,为其创建一个方法句柄。
· if reference_kind项的值是 9 (REF_invokeInterface),那么该索引处的 constant_pool条目必须是一个 CONSTANT_InterfaceMethodref_info结构,代表一个接口的方法,为其创建一个方法柄。
ifreference_kind项的值是5(REF_invokeVirtual)、6(REF_invokeStatic)、7(REF_invokeSpecial)或9(REF_invokeInterface),则CONSTANT_Methodref_info结构或CONSTANT_InterfaceMethodref_info结构所代表的方法名称不能是<init>或<clinit>。
if值为8(REF_newInvokeSpecial),CONSTANT_Methodref_info结构所代表的方法名称必须为<init>。
4.4.9.CONSTANT_MethodType_info结构
CONSTANT_MethodType_info结构用于表示一个方法类型。
CONSTANT_MethodType_info {
u1标签。
u2 descriptor_index。
}
CONSTANT_MethodType_info结构的项目如下。
标签
CONSTANT_MethodType_info结构的标签项的值是CONSTANT_MethodType(16)。
描述符_索引
descriptor_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表一个方法描述符(§4.3.3)。
4.4.10.CONSTANT_InvokeDynamic_info结构
CONSTANT_InvokeDynamic_info结构被一个invokedynamic指令(§invokedynamic)用来指定一个引导方法、动态调用名称、调用的参数和返回类型,以及可选的、被称为引导方法的静态参数的额外常数序列。
CONSTANT_InvokeDynamic_info {
u1标签。
u2 bootstrap_method_attr_index。
u2 name_and_type_index。
}
CONSTANT_InvokeDynamic_info结构的项目如下。
标签
CONSTANT_InvokeDynamic_info结构的标签项具有CONSTANT_InvokeDynamic(18)的值。
采集方法的索引
bootstrap_method_attr_index项的值必须是这个类文件中bootstrap方法表(§4.7.23)的bootstrap_methods数组的有效索引。
名称和类型索引
name_and_type_index项的值必须是 constant_pool表的一个有效索引。该索引中的 constant_pool条目必须是一个 CONSTANT_NameAndType_info结构(§4.4.6),代表一个方法名称和方法描述符(§4.3.3)。
4.5.属性信息
每个字段都由一个field_info结构来描述。
一个类文件中没有两个字段可以有相同的名称和描述符(§4.3.2)。
该结构具有以下格式。
Field_info {
u2 access_flags。
u2 name_index。
u2 descriptor_index。
u2 attributes_count。
attribute_info属性[attribute_count]。
}
field_info结构的项目如下。
访问_flags
access_flags项的值是一个标志的掩码,用于表示对这个字段的访问权限和属性。表4.5-A中规定了每个标志在设置时的解释。
表4.5-A.实地访问和财产标志
| 名称 | 值 | 解释 |
|---|---|---|
ACC_PUBLIC | 0x0001 | 宣布为公共的;可以从它的包之外访问。 |
ACC_PRIVATE | 0x0002 | 宣告为私有;仅在定义类中可用。 |
ACC_PROTECTED | 0x0004 | 宣布为受保护;可以在子类中访问。 |
ACC_STATIC | 0x0008 | 宣布为静态。 |
ACC_FINAL | 0x0010 | 被宣布为最终的;在对象构造之后,永远不会直接分配到(JLS §17.5)。 |
ACC_VOLATILE | 0x0040 | 宣布为易失性;不能被缓存。 |
ACC_TRANSIENT | 0x0080 | 宣布为瞬时的;不被持久性对象管理器写入或读取。 |
ACC_SYNTHETIC | 0x1000 | 宣称是合成的;在源代码中不存在。 |
ACC_ENUM | 0x4000 | 宣布为一个枚举的元素。 |
类的字段可以设置表4.5-A中的任何标志。然而,一个类的每个字段最多可以设置ACC_PUBLIC,ACC_PRIVATE和ACC_PROTECTED中的一个标志(JLS §8.3.1),并且不能同时设置ACC_FINAL和ACC_VOLATILE标志(JLS §8.3.1.4)。
接口的字段必须设置ACC_PUBLIC、ACC_STATIC和ACC_FINAL标志;它们可以设置ACC_SYNTHETIC标志,并且不能设置表4.5-A中的任何其他标志(JLS §9.3)。
ACC_SYNTHETIC标志表示这个字段是由编译器生成的,不在源代码中出现。
ACC_ENUM标志表示这个字段是用来保存一个枚举类型的元素的。
表4.5-A中未分配的access_flags项的所有位都保留给将来使用。它们应该在生成的类文件中被设置为零,并且应该被Java虚拟机的实现所忽略。
名称_索引
name_index项的值必须是 constant_pool表的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表一个有效的表示字段的非限定名称(§4.2.2)。
描述符_索引
descriptor_index项的值必须是constant_pool表中的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表一个有效的字段描述符(§4.3.2)。
属性_count
attributes_count项的值表示这个字段的附加属性的数量。
属性[]
属性表的每个值必须是一个attribute_info结构(§4.7)。
一个字段可以有任何数量的可选属性与之相关。
表4.7-C中列出了本规范定义的出现在field_info结构的属性表中的属性。
关于定义在field_info结构的属性表中出现的属性的规则在§4.7中给出。
关于field_info结构的属性表中的非预定义属性的规则在§4.7.1中给出。
4.6.方法
每个方法,包括每个实例初始化方法(§2.9)和类或接口初始化方法(§2.9),都由method_info结构来描述。
一个类文件中没有两个方法可以有相同的名称和描述符(§4.3.3)。
该结构具有以下格式。
Method_info {
u2 access_flags。
u2 name_index。
u2 descriptor_index。
u2 attributes_count。
attribute_info属性[attribute_count]。
}
method_info结构的项目如下。
访问_flags
access_flags项的值是一个标志的掩码,用于表示对该方法的访问许可和属性。表4.6-A中规定了每个标志在设置时的解释。
表4.6-A.方法访问和属性标志
| 名称 | 值 | 解释 |
|---|---|---|
ACC_PUBLIC | 0x0001 | 宣布为公共的;可以从它的包之外访问。 |
ACC_PRIVATE | 0x0002 | 宣布为私有;只能在定义类中访问。 |
ACC_PROTECTED | 0x0004 | 宣布为受保护;可以在子类中访问。 |
ACC_STATIC | 0x0008 | 宣布为静态。 |
ACC_FINAL | 0x0010 | 宣布为最终值;不能被重写(§5.4.5)。 |
acc_synchronized | 0x0020 | 宣称是同步的;调用被一个监视器的使用所包裹。 |
ACC_BRIDGE | 0x0040 | 一个桥梁方法,由编译器生成。 |
ACC_VARARGS | 0x0080 | 以可变的参数数声明。 |
ACC_NATIVE | 0x0100 | 宣称是本地的;用Java以外的语言实现。 |
ACC_ABSTRACT | 0x0400 | 宣布为抽象的;没有提供实现。 |
ACC_STRICT | 0x0800 | 宣告为strictfp;浮点模式为FP-strict。 |
ACC_SYNTHETIC | 0x1000 | 宣称是合成的;在源代码中不存在。 |
类的方法可以设置表4.6-A中的任何标志。然而,一个类的每个方法最多可以设置ACC_PUBLIC、ACC_PRIVATE和ACC_PROTECTED中的一个标志(JLS §8.4.3)。
除了ACC_PROTECTED, ACC_FINAL, ACC_SYNCHRONIZED和ACC_NATIVE(JLS §9.4),接口的方法可以设置表4.6-A中的任何标志。在一个版本号小于52.0的类文件中,接口的每个方法都必须设置ACC_PUBLIC和ACC_ABSTRACT标志;在一个版本号大于等于52.0的类文件中,接口的每个方法都必须设置ACC_PUBLIC和ACC_PRIVATE标志中的一个。
if一个类或接口的方法设置了ACC_ABSTRACT标志,它必须没有设置任何ACC_PRIVATE、ACC_STATIC、ACC_FINAL、ACC_SYNCHRONIZED、ACC_NATIVE或ACC_STRICT标志。
每个实例初始化方法(§2.9)最多可以设置一个ACC_PUBLIC、ACC_PRIVATE和ACC_PROTECTED标志,也可以设置ACC_VARARGS、ACC_STRICT和ACC_SYNTHETIC标志,但不能设置表4.6-A中的任何其他标志。
类和接口的初始化方法是由Java虚拟机隐式调用的。它们的access_flags项的值被忽略,除了ACC_STRICT标志的设置。
ACC_BRIDGE标志用于表示由Java编程语言的编译器生成的桥接方法。
ACC_VARARGS标志表示该方法在源代码层面上需要一个可变数量的参数。一个被声明为需要可变参数数的方法必须在编译时将ACC_VARARGS标志设置为1。所有其他方法必须在编译时将ACC_VARARGS标志设为0。
ACC_SYNTHETIC标志表示这个方法是由编译器生成的,不会出现在源代码中,除非它是§4.7.8中命名的方法之一。
表4.6-A中未分配的access_flags项的所有位都保留给未来使用。它们应该在生成的类文件中被设置为零,并且应该被Java虚拟机的实现所忽略。
名称_索引
name_index项的值必须是 constant_pool表的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表特殊方法名称<init>或<clinit>之一(§2.9),或者是一个表示方法的有效非限定名称(§4.2.2)。
描述符_索引
descriptor_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构,代表一个有效的方法描述符(§4.3.3)。
本规范的未来版本可能会要求,ifaccess_flags项中设置了ACC_VARARGS标志,那么方法描述符的最后一个参数描述符就是一个数组类型。
属性_count
attributes_count项的值表示这个方法的附加属性的数量。
属性[]
属性表的每个值必须是一个attribute_info结构(§4.7)。
一个方法可以有任何数量的可选属性与之相关。
表4.7-C中列出了本规范定义的出现在method_info结构的属性表中的属性。
关于定义在method_info结构的属性表中出现的属性的规则在§4.7中给出。
关于method_info结构的属性表中的非预定义属性的规则在§4.7.1中给出。
4.7.属性
属性在类文件格式的ClassFile、field_info、method_info和Code_attribute结构中使用(§4.1、§4.5、§4.6和§4.7.3)。
所有属性都有以下一般格式。
属性_信息 {
u2属性_名称_索引。
u4 attribute_length。
u1信息[属性_长度]。
}
对于所有的属性,attribute_name_index必须是该类的常量池中的一个有效的无符号16位索引。attribute_name_index处的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表属性的名称。attribute_length项的值表示后续信息的长度,单位是字节。这个长度不包括最初的六个字节,这六个字节包含了attributee_name_index和attributee_length项。
本规范预定义了23个属性。为了便于浏览,它们被列举了三次。
· Table 4.7-A是按照属性在本章中的章节编号排序的。每个属性都伴随着它所定义的类文件格式的第一个版本,以及相应的 Java SE 平台的版本。(对于旧的类文件版本,使用 JDK 版本而不是 Java SE 平台版本)。
· Table 4.7-B是按照每个属性被定义的第一个类文件格式的版本来排序的。
· Table 4.7-C是按照每个属性在类文件中被定义为出现的位置排序的。
在本规范的使用范围内,也就是在它们出现的类文件结构的属性表中,这些预定义属性的名称被保留。
在属性表中出现预定义属性的任何条件都在描述该属性的部分明确指定。if没有指定条件,那么该属性可以在属性表中出现任何次数。
预定义的属性根据其目的被分为三组。
\1. 有五个属性对Java虚拟机正确解释类文件至关重要。
· 恒定值
· 编码
· 堆栈地图表
· 例外情况
· 靴带方法
在版本V的类文件中,ifJava虚拟机的实现能够识别版本V的类文件,并且版本V至少是首次定义该属性的版本,并且该属性出现在定义的位置,那么这些属性必须被Java虚拟机的实现识别并正确读取。
\2. 12个属性对于Java SE平台的类库正确解释类文件至关重要。
· 内层类目
· 包围方法
· 合成的
· 签名
· 运行时可见注释
· 运行时不可见注释
· 运行时可见的参数注解
· 运行时不可见参数注解
· 运行时可见类型注释(RuntimeVisibleTypeAnnotations
· 运行时不可见类型注解
· 注释Default
· 方法参数
ifJava SE平台的类库实现能够识别版本V的类文件,并且版本V至少是首次定义该属性的版本,并且该属性出现在定义的位置,那么版本V的类文件中的每个属性都必须被该类库实现识别并正确读取。
\3. 六个属性对于Java虚拟机或Java SE平台的类库对类文件的正确解释并不关键,但对于工具来说是有用的。
· 源文件
· 源代码调试扩展(SourceDebugExtension
· 线号表
· 本地变量表(LocalVariableTable
· 本地变量类型表(LocalVariableTypeTable
· 废弃的
Java虚拟机的实现或Java SE平台的类库对这些属性的使用是可选的。一个实现可以使用这些属性所包含的信息,否则必须默默地忽略这些属性。
表4.7-A.预定义的类文件属性(按章节划分)
| 属性 | 科目 | 类文件 | Java SE |
|---|---|---|---|
恒定值 | §4.7.2 | 45.3 | 1.0.2 |
编码 | §4.7.3 | 45.3 | 1.0.2 |
堆栈地图表(StackMapTable | §4.7.4 | 50.0 | 6 |
例外情况 | §4.7.5 | 45.3 | 1.0.2 |
内层类目 | §4.7.6 | 45.3 | 1.1 |
包围方法 | §4.7.7 | 49.0 | 5.0 |
合成的 | §4.7.8 | 45.3 | 1.1 |
签名 | §4.7.9 | 49.0 | 5.0 |
源文件 | §4.7.10 | 45.3 | 1.0.2 |
源代码调试扩展(SourceDebugExtension | §4.7.11 | 49.0 | 5.0 |
线号表 | §4.7.12 | 45.3 | 1.0.2 |
本地变量表(LocalVariableTable | §4.7.13 | 45.3 | 1.0.2 |
本地变量类型表(LocalVariableTypeTable | §4.7.14 | 49.0 | 5.0 |
废弃的 | §4.7.15 | 45.3 | 1.1 |
运行时可见注释 | §4.7.16 | 49.0 | 5.0 |
运行时不可见注释 | §4.7.17 | 49.0 | 5.0 |
运行时可见的参数注解 | §4.7.18 | 49.0 | 5.0 |
运行时不可见参数注解 | §4.7.19 | 49.0 | 5.0 |
运行时可见类型注释(RuntimeVisibleTypeAnnotations | §4.7.20 | 52.0 | 8 |
运行时不可见类型注解 | §4.7.21 | 52.0 | 8 |
注释Default | §4.7.22 | 49.0 | 5.0 |
靴带方法 | §4.7.23 | 51.0 | 7 |
方法参数 | §4.7.24 | 52.0 | 8 |
表4.7-B.预定义的类文件属性(按类文件版本划分)
| 属性 | 类文件 | Java SE | 科目 |
|---|---|---|---|
恒定值 | 45.3 | 1.0.2 | §4.7.2 |
编码 | 45.3 | 1.0.2 | §4.7.3 |
例外情况 | 45.3 | 1.0.2 | §4.7.5 |
源文件 | 45.3 | 1.0.2 | §4.7.10 |
线号表 | 45.3 | 1.0.2 | §4.7.12 |
本地变量表(LocalVariableTable | 45.3 | 1.0.2 | §4.7.13 |
内部类 | 45.3 | 1.1 | §4.7.6 |
合成的 | 45.3 | 1.1 | §4.7.8 |
废弃的 | 45.3 | 1.1 | §4.7.15 |
包围方法 | 49.0 | 5.0 | §4.7.7 |
签名 | 49.0 | 5.0 | §4.7.9 |
源代码调试扩展(SourceDebugExtension | 49.0 | 5.0 | §4.7.11 |
本地变量类型表(LocalVariableTypeTable | 49.0 | 5.0 | §4.7.14 |
运行时可见注释 | 49.0 | 5.0 | §4.7.16 |
运行时不可见注释 | 49.0 | 5.0 | §4.7.17 |
运行时可见的参数注解 | 49.0 | 5.0 | §4.7.18 |
运行时不可见参数注解 | 49.0 | 5.0 | §4.7.19 |
注释Default | 49.0 | 5.0 | §4.7.22 |
堆栈地图表 | 50.0 | 6 | §4.7.4 |
靴带方法 | 51.0 | 7 | §4.7.23 |
运行时可见类型注释(RuntimeVisibleTypeAnnotations | 52.0 | 8 | §4.7.20 |
运行时不可见类型注解 | 52.0 | 8 | §4.7.21 |
方法参数 | 52.0 | 8 | §4.7.24 |
表4.7-C.预定义的类文件属性(按位置划分)
| 属性 | 地点 | 类文件 |
|---|---|---|
源文件 | 类文件 | 45.3 |
内层类目 | 类文件 | 45.3 |
包围方法 | 类文件 | 49.0 |
源代码调试扩展(SourceDebugExtension | 类文件 | 49.0 |
靴带方法 | 类文件 | 51.0 |
恒定值 | 栏位_信息 | 45.3 |
编码 | Method_info | 45.3 |
例外情况 | Method_info | 45.3 |
RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations | Method_info | 49.0 |
注释Default | Method_info | 49.0 |
方法参数 | Method_info | 52.0 |
合成的 | 类文件,字段_信息,方法_信息 | 45.3 |
废弃的 | 类文件,字段_信息,方法_信息 | 45.3 |
签名 | 类文件,字段_信息,方法_信息 | 49.0 |
运行时可见注解,运行时不可见注解 | 类文件,字段_信息,方法_信息 | 49.0 |
线号表 | 编码 | 45.3 |
本地变量表(LocalVariableTable | 编码 | 45.3 |
本地变量类型表(LocalVariableTypeTable | 编码 | 49.0 |
堆栈地图表(StackMapTable | 编码 | 50.0 |
RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations | 类文件,字段信息,方法信息,代码 | 52.0 |
4.7.1.定义和命名新属性
编译器被允许定义和发出包含类文件结构、field_info结构、method_info结构和Code属性(§4.7.3)的属性表中的新属性的类文件。Java 虚拟机实现被允许识别和使用在这些属性表中发现的新属性。然而,任何未定义为类文件规范一部分的属性都不能影响类文件的语义。Java 虚拟机实现需要默默地忽略它们不认识的属性。
例如,定义一个新的属性来支持特定供应商的调试是允许的。因为Java虚拟机实现被要求忽略他们不认识的属性,所以为该特定Java虚拟机实现准备的类文件将被其他实现所使用,即使这些实现不能利用类文件包含的额外调试信息。
Java虚拟机的实现被明确禁止抛出异常或者仅仅因为存在一些新的属性而拒绝使用类文件。当然,if给出的类文件不包含它们所需的所有属性,在类文件上操作的工具可能无法正确运行。
两个本来是不同的属性,但碰巧使用了相同的属性名称和相同的长度,在识别任何一个属性的实现中都会发生冲突。在本规范之外定义的属性必须根据《Java 语言规范,Java SE 8 版》(JLS 第 6.1 节)中描述的包命名惯例选择名称。
本规范的未来版本可能会定义额外的属性。
4.7.2.ConstantValue属性
ConstantValue属性是field_info结构的属性表中的一个固定长度的属性(§4.5)。ConstantValue属性代表一个常量表达式的值(JLS §15.28),其使用方法如下。
· iffield_info结构的access_flags项中的ACC_STATIC标志被设置,那么field_info结构所代表的字段就会被分配其ConstantValue属性所代表的值,作为声明该字段的类或接口的初始化的一部分(§5.5)。这发生在调用该类或接口的初始化方法之前(§2.9)。
· 否则,Java虚拟机必须默默地忽略该属性。
在一个field_info结构的属性表中最多可以有一个ConstantValue属性。
ConstantValue属性有以下格式。
ConstantValue_attribute {
u2属性_名称_索引。
u4 attribute_length。
u2 constantvalue_index。
}
ConstantValue_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是 constant_pool表的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “ConstantValue”。
属性_长度
ConstantValue_attribute结构的attribute_length项的值必须是2。
常量值_索引
constantvalue_index项的值必须是constant_pool表中的一个有效索引。该索引中的constant_pool条目给出了该属性所代表的常量值。constant_pool条目必须是适合该字段的类型,如表4.7.2-A所规定。
表4.7.2-A.恒定值属性类型
| 字段类型 | 入场类型 |
|---|---|
长 | CONSTANT_LONG |
浮动 | CONSTANT_Float |
双 | CONSTANT_Double |
int, short, char, byte, boolean | CONSTANT_Integer |
字符串 | CONSTANT_String |
4.7.3.代码属性
Code属性是method_info结构的属性表中的一个可变长度的属性(§4.6)。Code属性包含了一个方法的Java虚拟机指令和辅助信息,包括一个实例初始化方法或一个类或接口初始化方法(§2.9)。
if该方法是本地的或抽象的,它的method_info结构在其属性表中不能有一个Code属性。否则,它的method_info结构在其属性表中必须有一个确切的Code属性。
代码属性有以下格式。
代码_属性 {
u2属性_名称_索引。
u4 attribute_length。
u2 max_stack。
u2 max_locals。
u4 code_length。
u1代码[code_length]。
u2 exception_table_length。
{ u2 start_pc;
u2 end_pc。
u2 handler_pc。
u2 catch_type。
} exception_table[exception_table_length]。
u2 attributes_count。
attribute_info属性[attribute_count]。
}
Code_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “Code”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
max_stack
max_stack项的值给出了该方法在执行过程中任何时候的操作数栈的最大深度(§2.6.2)。
最大局部
max_locals项的值给出了调用该方法时分配的局部变量数组中的局部变量数量(§2.6.1),包括在调用该方法时用于向其传递参数的局部变量。
long或double类型的值的最大局部变量索引是max_locals - 2.任何其他类型的值的最大局部变量索引是max_locals - 1.
代码长度
code_length项的值给出了这个方法的代码数组中的字节数。
code_length的值必须大于0(因为代码数组不能为空)且小于65536。
代码[]
代码数组给出了实现该方法的Java虚拟机代码的实际字节数。
当代码阵列被读入可字节寻址机器的内存时,if阵列的第一个字节在4字节边界上对齐,表开关和查找开关的32位偏移量将是4字节对齐。(关于代码阵列对齐的后果,请参考这些指令的描述)。
对代码数组内容的详细约束是广泛的,在另一节中给出(§4.9)。
异常表长度
exception_table_length项的值给出了exception_table表中的条目数。
异常表[]
exception_table数组中的每个条目描述了代码数组中的一个异常处理程序。异常表数组中处理程序的顺序是重要的(§2.10)。
每个exception_table条目包含以下四个项目。
start_pc, end_pc
start_pc和end_pc两个项目的值表示异常处理程序在代码数组中的活动范围。start_pc的值必须是指令操作码的代码数组中的有效索引。end_pc的值必须是指令操作码的代码数组中的有效索引,或者必须等于代码数组的长度code_length。start_pc的值必须小于end_pc的值。
start_pc是包容性的,end_pc是排他性的;也就是说,当程序计数器在[start_pc, end_pc]这个区间内时,异常处理程序必须是活动的。
end_pc是排他性的,这是Java虚拟机设计中的一个历史性错误:if一个方法的Java虚拟机代码正好是65535字节长,并且以1字节长的指令结束,那么该指令不能被异常处理程序保护。编译器作者可以通过限制任何方法、实例初始化方法或静态初始化器生成的Java虚拟机代码的最大尺寸(任何代码阵列的尺寸)来解决这个错误。
handler_pc
handler_pc项的值表示异常处理程序的开始。该项的值必须是代码数组的有效索引,并且必须是一条指令的操作码的索引。
捕获_类型
ifcatch_type项的值为非零,它必须是constant_pool表中的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Class_info结构(§4.4.1),代表这个异常处理程序被指定用来捕获的一类异常。只有当抛出的异常是给定的类或其子类中的一个实例时,才会调用这个异常处理程序。
验证器检查类是否是Throwable或Throwable的子类(§4.9.2)。
ifcatch_type项的值为0,这个异常处理程序就会被调用,用于处理所有的异常。
这被用来实现最终(§3.13)。
属性_count
attributes_count项的值表示Code属性的属性数量。
属性[]
属性表的每个值必须是一个attribute_info结构(§4.7)。
一个Code属性可以有任何数量的可选属性与之相关。
表4.7-C中列出了本规范所定义的出现在Code属性的属性表中的属性。
关于定义在代码属性的属性表中出现的属性的规则在§4.7中给出。
关于Code属性的属性表中的非预定义属性的规则在§4.7.1中给出。
4.7.4.StackMapTable属性
StackMapTable属性是Code属性的属性表中的一个可变长度的属性(§4.7.3)。StackMapTable属性在类型检查的验证过程中被使用(§4.10.1)。
在一个Code属性的属性表中最多可以有一个StackMapTable属性。
在一个版本号为50.0或以上的类文件中,if一个方法的Code属性没有StackMapTable属性,那么它就有一个隐式堆栈映射属性(§4.10.1)。这个隐式堆栈映射属性等同于一个 number_of_entries等于零的 StackMapTable属性。
StackMapTable属性有以下格式。
StackMapTable_attribute {
u2属性_名称_索引。
u4 attribute_length。
u2 number_of_entries;
堆积图_框架条目[number_of_entries]。
}
StackMapTable_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “StackMapTable”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
数目
number_of_entries项的值给出了条目表中的stack_map_frame条目的数量。
条目[]
条目表中的每个条目描述了该方法的一个堆栈映射框架。条目表中的堆栈图框的顺序是很重要的。
一个堆栈映射框架指定了(明确地或隐含地)它所适用的字节码偏移量,以及该偏移量的局部变量和操作数堆栈条目的验证类型。
条目表中描述的每个堆栈映射框架都依赖于前一个框架的一些语义。一个方法的第一个堆栈映射框架是隐含的,由类型检查器从方法描述符中计算出来(§4.10.1.6)。因此,位于 entries[0]的 stack_map_frame结构描述了方法的第二个堆栈映射框架。
堆栈映射框架适用的字节码偏移量的计算方法是:取框架中指定的值 offset_delta(无论是显式还是隐式),然后在前一帧的字节码偏移量上加上 offset_delta + 1,除非前一帧是该方法的初始帧。在这种情况下,堆栈映射框架适用的字节码偏移量是框架中指定的 offset_delta 值。
通过使用偏移量delta而不是存储实际的字节码偏移量,根据定义,我们确保堆栈映射帧处于正确的排序顺序。此外,通过对所有显式帧(而不是隐式第一帧)始终使用offset_delta + 1的公式,我们保证了没有重复。
我们说字节码中的一条指令有一个相应的堆栈映射框架,if该指令从代码属性的代码数组中的偏移量i开始,并且代码属性有一个StackMapTable属性,其条目数组包含一个堆栈映射框架,适用于字节码偏移量i。
一个验证类型指定了一个或两个位置的类型,其中一个位置是单个局部变量或单个操作数堆栈条目。一个验证类型由一个区分的联合体表示,即verification_type_info,它由一个一字节的标签组成,表示联合体的哪一项正在使用,后面是零个或多个字节,提供关于该标签的更多信息。
联盟 verification_type_info {
Top_variable_info;
整数_变量_信息。
Float_variable_info。
Long_variable_info;
Double_variable_info;
Null_variable_info。
未初始化这个变量的信息。
对象_变量_信息。
未被初始化的变量信息(Uninitialized_variable_info)。
}
在局部变量数组或操作数堆栈中指定一个位置的验证类型,由verification_type_info联盟的下列项目表示。
· Top_variable_info项表示局部变量具有验证类型top。
· Top_variable_info {
· u1 tag = ITEM_Top; / 0 /
· }
· Integer_variable_info项表示该位置具有验证类型int。
· 整数_变量_信息 {
· u1 tag = ITEM_Integer; / 1 /
· }
· Float_variable_info项表示该位置具有验证类型float。
· Float_variable_info {
· u1 tag = ITEM_Float; / 2 /
· }
· Null_variable_info类型表示该位置具有验证类型null。
· Null_variable_info {
· u1 tag = ITEM_Null; / 5 /
· }
· UninitializedThis_variable_info项表示该位置具有验证类型uninitializedThis。
· 未初始化的这一变量的信息 {
· u1 tag = ITEM_UninitializedThis; / 6 /
· }
· Object_variable_info项表示该位置具有验证类型,该验证类型是CONSTANT_Class_info结构(§4.4.1)所代表的类,该结构在constant_pool表中找到,索引由cpool_index给出。
· 对象_变量_信息 {
· u1 tag = ITEM_Object; / 7 /
· u2 cpool_index。
· }
· Uninitialized_variable_info项表示该位置具有验证类型uninitialized(Offset)。Offset项表示在包含这个StackMapTable属性的Code属性的代码数组中,创建被存储在该位置的对象的新指令(§new)的偏移。
· 未初始化的变量信息 {
· u1 tag = ITEM_Uninitialized; / 8 /
· u2偏移。
· }
一个在局部变量数组或操作数堆中指定两个位置的验证类型,由verification_type_info联盟的下列项目表示。
· Long_variable_info项表示两个位置中的第一个具有验证类型long。
· Long_variable_info {
· u1 tag = ITEM_Long; / 4 /
· }
· Double_variable_info项表示两个位置中的第一个具有验证类型double。
· Double_variable_info {
· u1 tag = ITEM_Double; / 3 /
· }
· Long_variable_info和Double_variable_info项表示两个位置中第二个的验证类型,如下所示。
o if两个位置中的第一个是一个局部变量,那么。
§ 它不能是具有最高索引的局部变量。
§ 下一个更高编号的局部变量具有验证类型top。
o if两个位置中的第一个是一个操作数堆栈条目,那么。
§ 它不能是操作数堆栈的最顶端位置。
§ 靠近操作数堆栈顶部的下一个位置具有验证类型top。
一个堆栈映射框架由一个区分的联合体表示,即堆栈映射框架,它由一个一字节的标签组成,表示联合体的哪一项正在使用,后面是零个或多个字节,给出关于该标签的更多信息。
联盟 stack_map_frame {
同一个框架。
同位数_1_堆栈_项目_框架。
同位数_1_堆栈_项目_框架_扩展。
chop_frame。
同样的_frame_extended。
append_frame。
full_frame;
}
标签表示堆栈映射框架的框架类型。
· 帧类型same_frame由范围为[0-63]的标记表示。这种帧类型表示该帧具有与前一帧完全相同的局部变量,并且操作数栈为空。帧的 offset_delta值是标签项 frame_type的值。
· 同一个框架 {
· u1 frame_type = SAME; / 0-63 /
· }
· 框架类型same_locals_1_stack_item_frame由范围为[64, 127]的标记表示。这种框架类型表示该框架具有与前一个框架完全相同的局部变量,并且操作数栈有一个条目。该帧的offset_delta值由公式frame_type-64给出。一个堆栈条目的验证类型出现在帧类型之后。
· 同样的_locals_1_stack_item_frame {
· u1 frame_type = SAME_LOCALS_1_STACK_ITEM; / 64-127 /
· verification_type_info stack[1];
· }
· 在[128-246]范围内的标签被保留下来供将来使用。
· 帧类型same_locals_1_stack_item_frame_extended由标签247表示。这种框架类型表示该框架具有与前一个框架完全相同的局部变量,并且操作数栈有一个条目。该帧的offset_delta值是明确给出的,与帧类型same_locals_1_stack_item_frame不同。一个堆栈条目的验证类型出现在 offset_delta之后。
· 相同_locals_1_stack_item_frame_extended {
· u1 frame_type = SAME_LOCALS_1_STACK_ITEM_EXTENDED; / 247 /
· u2 offset_delta。
· verification_type_info stack[1];
· }
· 帧类型chop_frame由范围为[248-250]的标记表示。这种帧类型表示该帧具有与前一帧相同的局部变量,只是最后的k个局部变量不存在,而且操作数栈是空的。k的值由公式251 - frame_type给出。帧的offset_delta值是明确给出的。
· 劈腿{
· u1 frame_type = CHOP; / 248-250 /
· u2 offset_delta。
· }
假设前一帧中局部变量的验证类型是由locals给出的,这是一个与full_frame帧类型结构相同的数组。if前一帧中的locals[M-1]代表局部变量X,locals[M]代表局部变量Y,那么删除一个局部变量的效果是,新帧中的locals[M-1]代表局部变量X,locals[M]未定义。
ifk大于前一帧的locals中的局部变量的数量,也就是说,if新帧中的局部变量的数量将小于0,那就是一个错误。
· 帧类型same_frame_extended是由标记251表示的。这个框架类型表示该框架具有与前一个框架完全相同的局部变量,并且操作数栈是空的。帧的offset_delta值是明确给出的,与帧类型same_frame不同。
· 相同的框架_扩展的 {
· u1 frame_type = SAME_FRAME_EXTENDED; / 251 /
· u2 offset_delta。
· }
· 帧类型append_frame由范围为[252-254]的标记表示。这种框架类型表明,除了定义了k个额外的locals之外,该框架具有与前一个框架相同的locals,并且操作数栈是空的。k的值由公式 frame_type - 251给出。帧的offset_delta值是明确给出的。
· append_frame {
· u1 frame_type = APPEND; / 252-254 /
· u2 offset_delta。
· verification_type_info locals[frame_type - 251]。
· }
locals中的第0个条目代表第一个附加局部变量的验证类型。iflocals[M]代表局部变量N,那么。
o iflocals[M]是Top_variable_info、Integer_variable_info、Float_variable_info、Null_variable_info、UninitializedThis_variable_info、Object_variable_info或Uninitialized_variable_info中的一种,locals[M+1]代表局部变量N+1;并且
o iflocals[M]是Long_variable_info或Double_variable_info,locals[M+1]代表局部变量N+2。
if对于任何索引i,locals[i]代表的局部变量的索引大于方法的局部变量的最大数量,则是一个错误。
· 帧类型full_frame由标签255表示。帧的offset_delta值是明确给出的。
· full_frame {
· u1 frame_type = FULL_FRAME; / 255 /
· u2 offset_delta。
· u2 number_of_locals。
· verification_type_info locals[number_of_locals]。
· u2 number_of_stack_items;
· verification_type_info stack[number_of_stack_items]。
· }
locals中的第0个条目代表局部变量0的验证类型。 iflocals[M]代表局部变量N,那么。
o iflocals[M]是Top_variable_info、Integer_variable_info、Float_variable_info、Null_variable_info、UninitializedThis_variable_info、Object_variable_info或Uninitialized_variable_info中的一种,locals[M+1]代表局部变量N+1;并且
o iflocals[M]是Long_variable_info或Double_variable_info,locals[M+1]代表局部变量N+2。
if对于任何索引i,locals[i]代表的局部变量的索引大于方法的局部变量的最大数量,则是一个错误。
堆栈的第0个条目代表操作数堆栈底部的验证类型,堆栈的后续条目代表更接近操作数堆栈顶部的堆栈条目的验证类型。我们把操作数栈的底部称为栈条目0,把操作数栈的后续条目称为栈条目1、2等。ifstack[M]代表堆栈条目N,那么。
o if堆栈[M]是Top_variable_info、Integer_variable_info、Float_variable_info、Null_variable_info、UninitializedThis_variable_info、Object_variable_info或Uninitialized_variable_info中的一种,则堆栈[M+1]代表堆栈条目N+1;并且
o ifstack[M]是Long_variable_info或Double_variable_info,则stack[M+1]代表stack entry N+2。
if对于任何索引i,stack[i]代表一个堆栈条目,其索引大于方法的最大操作数堆栈大小,则是一个错误。
4.7.5.异常属性
Exceptions属性是 method_info结构(§4.6)的属性表中的一个可变长度的属性。Exceptions属性表示一个方法可以抛出哪些被检查的异常。
在一个method_info结构的属性表中最多可以有一个Exceptions属性。
Exceptions属性有以下格式。
例外_属性 {
u2属性_名称_索引。
u4 attribute_length。
u2 number_of_exceptions。
u2 exception_index_table[number_of_exceptions]。
}
Exceptions_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “Exceptions”。
属性_长度
attribute_length项的值表示属性长度,不包括最初的六个字节。
例外数
number_of_exceptions项的值表示exception_index_table中的条目数量。
异常_索引表[]
exception_index_table数组中的每个值必须是 constant_pool表的一个有效索引。该索引中的 constant_pool条目必须是一个 CONSTANT_Class_info结构(§4.4.1),代表这个方法被声明为抛出的类类型。
一个方法只有在满足以下三个条件中的至少一个时,才应该抛出一个异常。
· 异常是RuntimeException或其子类中的一个实例。
· 异常是Error或其子类中的一个实例。
· 异常是刚才描述的异常_index_table中指定的异常类之一的实例,或者是它们的子类之一。
这些要求没有在Java虚拟机中强制执行,它们只在编译时强制执行。
4.7.6.InnerClasses属性
InnerClasses属性是 ClassFile结构的属性表中的一个可变长度的属性(§4.1)。
if一个类或接口 C 的常量池至少包含一个 CONSTANT_Class_info条目(§4.4.1),它代表一个不属于包的成员的类或接口,那么在 C 的 ClassFile结构的属性表中必须正好有一个 InnerClasses属性。
InnerClasses属性有以下格式。
内在类别_属性 {
u2属性_名称_索引。
u4 attribute_length。
u2 number_of_classes。
{ u2 inner_class_info_index;
u2 outer_class_info_index。
u2 inner_name_index。
u2 inner_class_access_flags。
} classes[number_of_classes];
}
InnerClasses_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是 constant_pool表的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “InnerClasses”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
类的数量
number_of_classes项的值表示classes数组中的条目数。
类[]
在constant_pool表中的每个CONSTANT_Class_info条目,if代表一个非包成员的类或接口C,则必须在classes数组中正好有一个对应条目。
if一个类或接口有属于类或接口的成员,它的 constant_pool表(以及它的 InnerClasses属性)必须引用每个这样的成员(JLS §13.1),即使该成员没有被类提到。
此外,每个嵌套类和嵌套接口的constant_pool表都必须引用它的包围类,所以总的来说,每个嵌套类和嵌套接口都会有每个包围类和每个自己的嵌套类和接口的InnerClasses信息。
类数组中的每个条目都包含以下四项。
内在_类_信息_索引
inner_class_info_index项的值必须是constant_pool表中的一个有效索引。在该索引处的constant_pool条目必须是一个代表C的CONSTANT_Class_info结构。在classes数组条目中的其余项目提供关于C的信息。
外层信息索引
ifC不是一个类或一个接口的成员(也就是说,ifC是一个顶层类或接口(JLS §7.6)或一个局部类(JLS §14.3)或一个匿名类(JLS §15.9.5)), outer_class_info_index项的值必须为0。
否则,outer_class_info_index项的值必须是constant_pool表中的一个有效索引,并且该索引中的条目必须是一个CONSTANT_Class_info结构,代表C是其成员的类或接口。
内在名称_索引
ifC是匿名的(JLS §15.9.5),inner_name_index项的值必须是0。
否则,inner_name_index项的值必须是constant_pool表中的一个有效索引,并且该索引中的条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),它代表C的原始简单名称,正如在编译这个类文件的源代码中给出的那样。
内在的_class_access_flags
inner_class_access_flags项的值是一个标志的掩码,用于表示对类或接口C的访问权限和属性,因为这个类文件是由源代码编译的。当源代码不可用时,它被编译器用来恢复原始信息。这些标志在表4.7.6-A中指定。
表4.7.6-A.嵌套类的访问和属性标志
| 旗帜名称 | 价值 | 解释 |
|---|---|---|
ACC_PUBLIC | 0x0001 | 在来源中标明或隐含公开。 |
ACC_PRIVATE | 0x0002 | 在来源中被标记为私人。 |
呼叫中心:ACC_PROTECTED | 0x0004 | 在源头上标记为受保护。 |
ACC_STATIC | 0x0008 | 在源码中标明或隐含静态。 |
ACC_FINAL | 0x0010 | 在源头上标记为最终。 |
ACC_INTERFACE | 0x0200 | 是源码中的一个接口。 |
ACC_ABSTRACT | 0x0400 | 在来源中标明或隐含抽象。 |
ACC_SYNTHETIC | 0x1000 | 宣称是合成的;在源代码中不存在。 |
ACC_ANNOTATION | 0x2000 | 作为一个注解类型被宣布。 |
ACC_ENUM | 0x4000 | 作为一个枚举类型被宣布。 |
在表4.7.6-A中没有分配的inner_class_access_flags项的所有位都被保留下来供将来使用。它们应该在生成的类文件中被设置为零,并且应该被Java虚拟机的实现所忽略。
if一个类文件的版本号是51.0或以上,并且在其属性表中有一个InnerClasses属性,那么对于InnerClasses属性的classes数组中的所有条目,ifinner_name_index项的值为0,则 outer_class_info_index项的值必须为0。
Oracle的Java虚拟机实现并不检查InnerClasses属性与代表该属性所引用的类或接口的类文件的一致性。
4.7.7.封闭式方法(EnclosingMethod)属性
EnclosingMethod属性是ClassFile结构的属性表中的一个固定长度的属性(§4.1)。一个类必须有一个EnclosingMethod属性,当且仅当它代表一个本地类或一个匿名类时(JLS §14.3, JLS §15.9.5)。
在 ClassFile结构的属性表中,最多可以有一个 EnclosingMethod属性。
EnclosingMethod属性有如下格式。
包容性方法_属性 {
u2属性_名称_索引。
u4 attribute_length。
u2 class_index;
u2 method_index。
}
EnclosingMethod_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是 constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “EnclosingMethod”。
属性_长度
attribute_length项的值必须是4。
类的索引
class_index项的值必须是constant_pool表中的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Class_info结构(§4.4.1),代表包围着当前类的声明的最内层类。
方法_索引
if当前类没有被一个方法或构造函数立即包围,那么method_index项的值必须为0。
特别是,if当前类在源代码中被实例初始化器、静态初始化器、实例变量初始化器或类变量初始化器紧紧包围,method_index必须为0。(前两个涉及到本地类和匿名类,而后两个涉及到在字段赋值的右侧声明的匿名类。)
否则,method_index项的值必须是constant_pool表中的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_NameAndType_info结构(§4.4.6),代表上述class_index属性所引用的类中方法的名称和类型。
Java编译器有责任确保通过method_index识别的方法确实是包含这个EnclosingMethod属性的类中最接近词法的包围方法。
4.7.8.合成属性
合成属性是ClassFile、field_info或method_info结构的属性表中的一个固定长度的属性(§4.1, §4.5, §4.6)。一个没有出现在源代码中的类成员必须使用合成属性来标记,否则它必须设置ACC_SYNTHETIC标志。这一要求的唯一例外是编译器生成的方法,这些方法不被视为实现工件,即代表Java编程语言默认构造器的实例初始化方法(§2.9)、类初始化方法(§2.9)以及Enum.values()和Enum.valueOf()方法。
JDK 1.1中引入了Synthetic属性,以支持嵌套类和接口。
合成属性有以下格式。
合成_属性 {
u2属性_名称_索引。
u4 attribute_length。
}
Synthetic_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是 constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “Synthetic”。
属性_长度
attribute_length项的值必须为零。
4.7.9.签名属性
Signature属性是ClassFile、field_info或method_info结构的属性表中的一个固定长度的属性(§4.1, §4.5, §4.6)。Signature属性记录了一个类、接口、构造函数、方法或字段的签名(§4.7.9.1),该类在 Java 编程语言中的声明使用类型变量或参数化类型。关于这些类型的详细信息,请参见《Java语言规范》,Java SE 8版。
签名属性有以下格式。
签名_属性 {
u2属性_名称_索引。
u4 attribute_length。
u2 签名_索引。
}
Signature_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是 constant_pool表的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “Signature”。
属性_长度
Signature_attribute结构的attribute_length项的值必须是2。
签名_索引
signature_index项的值必须是 constant_pool表的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),if这个Signature属性是ClassFile结构的一个属性,则代表一个类签名;if这个Signature属性是method_info结构的一个属性,则代表一个方法签名;否则就是一个域签名。
Oracle的Java虚拟机实现在类的加载或链接过程中不检查Signature属性的格式是否良好。相反,Signature属性是由Java SE平台类库的方法来检查的,这些方法暴露了类、接口、构造函数、方法和字段的通用签名。例子包括Class中的getGenericSuperclass和java.lang.reflect.Executable中的toGenericString。
4.7.9.1.签名
签名对用Java编程语言编写的声明进行编码,这些声明使用Java虚拟机类型系统之外的类型。它们支持反射和调试,以及在只有类文件的情况下进行编译。
Java编译器必须为任何类、接口、构造函数、方法或字段(其声明使用了类型变量或参数化类型)发出一个签名。具体来说,一个Java编译器必须发出。
· 任何类或接口声明的类签名,它要么是通用的,要么有一个参数化的类型作为超类或超接口,或者两者都是。
· 任何方法或构造函数声明的方法签名,它要么是泛型的,要么有一个类型变量或参数化类型作为返回类型或正式参数类型,要么在throws子句中有一个类型变量,或者它们的任何组合。
if一个方法或构造函数声明中的throws子句不涉及类型变量,那么编译器可以将该声明视为没有throws子句,以便发出一个方法签名。
· 任何字段、形式参数或局部变量声明的字段签名,其类型使用类型变量或参数化类型。
签名是用遵循§4.3.1符号的语法指定的。除了该符号之外。
· 语法[x]在生产的右侧表示x的零或一出现,也就是说,x是一个可选的符号。包含可选符号的备选方案实际上定义了两个备选方案:一个是省略可选符号的方案,一个是包含可选符号的方案。
· 一个非常长的右侧可以通过明确缩进第二行来继续。
该语法包括终端符号Identifier,用来表示由Java编译器生成的类型、字段、方法、形式参数、局部变量或类型变量的名称。这样的名字不能包含任何ASCII字符.;[ / < > :(也就是方法名称中禁止使用的字符(§4.2.2)和冒号),但可以包含在Java编程语言中不能出现的标识符(JLS§3.8)。
签名依赖于一个被称为类型签名的非终端的层次结构。
· 一个Java类型签名代表了Java编程语言的引用类型或原始类型。
JavaTypeSignature。
为方便起见,在此重复第4.3.2节中的以下制作。
基础类型。
(其中之一)
\ B C D F I J S Z
· 一个引用类型签名代表了Java编程语言的一个引用类型,即一个类或接口类型,一个类型变量,或一个数组类型。
一个类的类型签名代表一个(可能是参数化的)类或接口类型。一个类的类型签名必须被制定成可以可靠地映射到它所表示的类的二进制名称,方法是抹去任何类型参数并将每个.字符转换为$字符。
一个类型变量签名代表一个类型变量。
一个数组类型签名表示一个数组类型的一个维度。
参考类型签名。
类类型签名
\ TypeVariableSignature
\ 阵列类型签名
类类型签名。
L `[[PackageSpecifier](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-PackageSpecifier)] [SimpleClassTypeSignature](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-SimpleClassTypeSignature) {[ClassTypeSignatureSuffix](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-ClassTypeSignatureSuffix)} `;
包装规格。
Identifier /{PackageSpecifier}
SimpleClassTypeSignature。
识别器[类型参数]
TypeArguments:
< `[TypeArgument](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-TypeArgument) {[TypeArgument](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-TypeArgument)} `>。
TypeArgument:
WildcardIndicator。
+`
\ `-
ClassTypeSignatureSuffix。
TypeVariableSignature。
T`标识符 `;
ArrayTypeSignature。
一个类的签名编码了关于一个(可能是通用的)类声明的类型信息。它描述了该类的任何类型参数,并且列出了它的(可能是参数化的)直接超类和直接超接口,if有的话。一个类型参数由它的名字描述,后面是任何类的边界和接口的边界。
类签名。
[类型参数] SuperclassSignature {SuperinterfaceSignature}。
TypeParameters。
<TypeParameter {TypeParameter} >``的类型参数。
TypeParameter:
课堂教学。
接口绑定。
SuperclassSignature。
SuperinterfaceSignature。
方法签名编码了关于一个(可能是通用的)方法声明的类型信息。它描述了该方法的任何类型参数;任何形式参数的(可能是参数化的)类型;(可能是参数化的)返回类型,if有的话;以及该方法的throws子句中声明的任何异常的类型。
MethodSignature。
[TypeParameters] ({JavaTypeSignature} )结果 {ThrowsSignature}
结果。
抛出签名。
^类类型签名
\ ^TypeVariableSignature
为方便起见,在此重复第4.3.3节中的以下制作。
VoidDescriptor。
V
由于编译器生成的工件,一个方法的方法签名可能与该方法的方法描述符不完全对应(§4.3.3)。特别是,方法签名中的正式参数类型的数量可能少于方法描述符中的参数描述符的数量。
字段签名对字段、形式参数或局部变量声明的(可能是参数化)类型进行编码。
现场签名。
4.7.10.源文件属性
SourceFile属性是 ClassFile结构的属性表中的一个可选的固定长度的属性(§4.1)。
在一个ClassFile结构的属性表中,最多只能有一个SourceFile属性。
SourceFile属性有以下格式。
SourceFile_attribute {
u2属性_名称_索引。
u4 attribute_length。
u2 sourcefile_index。
}
SourceFile_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是 constant_pool表的一个有效索引。该索引中的 constant_pool条目必须是一个 CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “SourceFile”。
属性_长度
SourceFile_attribute结构的attribute_length项的值必须是2。
源文件_索引
sourcefile_index项的值必须是constant_pool表中的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表一个字符串。
sourcefile_index项所引用的字符串将被解释为表示编译该类文件的源文件的名称。它不会被解释为表示包含该文件的目录名称或该文件的绝对路径名称;这种特定平台的附加信息必须由运行时解释器或开发工具在实际使用该文件名时提供。
4.7.11.SourceDebugExtension属性
SourceDebugExtension属性是ClassFile结构的属性表中的一个可选的属性(§4.1)。
在ClassFile结构的属性表中,最多可以有一个SourceDebugExtension属性。
SourceDebugExtension属性有以下格式。
SourceDebugExtension_attribute {
u2属性_名称_索引。
u4 attribute_length。
u1 debug_extension[ attribute_length]。
}
SourceDebugExtension_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “SourceDebugExtension”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
debug_extension[]
debug_extension数组保存扩展的调试信息,对Java虚拟机没有语义影响。这些信息用修改过的UTF-8字符串表示(§4.4.7),没有结束的零字节。
请注意,debug_extension数组可以表示一个比String类的实例所能表示的更长的字符串。
4.7.12.LineNumberTable属性
LineNumberTable属性是Code属性(§4.7.3)的属性表中的一个可选的可变长度的属性。它可以被调试器用来确定代码数组的哪一部分对应于原始源文件中的某个行号。
if多个LineNumberTable属性存在于一个Code属性的属性表中,那么它们可以以任何顺序出现。
在Code属性的属性表中,源文件的每一行可以有一个以上的LineNumberTable属性。也就是说,LineNumberTable属性可以一起代表源文件的某一行,而且不需要与源行一一对应。
LineNumberTable属性有以下格式。
LineNumberTable_attribute {
u2属性_名称_索引。
u4 attribute_length。
u2 line_number_table_length。
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length]。
}
LineNumberTable_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “LineNumberTable”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
行数表长度
line_number_table_length项的值表示在line_number_table数组中的条目数量。
行数表[]
line_number_table数组中的每个条目表示原始源文件中的行号在代码数组中的某一点发生变化。每个line_number_table条目必须包含以下两个项目。
start_pc
start_pc项的值必须表示代码数组中的索引,原始源文件中新行的代码从这个索引开始。
start_pc的值必须小于Code属性的code_length项的值,这个LineNumberTable是一个属性。
线号
line_number项的值必须给出原始源文件中相应的行数。
4.7.13.LocalVariableTable属性
LocalVariableTable属性是Code属性(§4.7.3)的属性表中的一个可选的变量长度属性。它可以被调试器用来在方法的执行过程中确定一个给定的局部变量的值。
if多个LocalVariableTable属性存在于一个Code属性的属性表中,那么它们可以以任何顺序出现。
在一个代码属性的属性表中,每个局部变量不能超过一个LocalVariableTable属性。
LocalVariableTable属性有以下格式。
本地变量表_属性 {
u2属性_名称_索引。
u4 attribute_length。
u2 local_variable_table_length。
{ u2 start_pc;
u2长度。
u2 name_index。
u2 descriptor_index。
u2指数。
} local_variable_table[local_variable_table_length]。
}
LocalVariableTable_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是 constant_pool表的一个有效索引。该索引中的 constant_pool条目必须是一个 CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “LocalVariableTable”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
本地变量表的长度
local_variable_table_length项的值表示local_variable_table数组中的条目数量。
本地_变量表[]
local_variable_table数组中的每个条目都表示一个代码数组偏移量的范围,在这个范围内有一个局部变量的值。它还表示在当前帧的局部变量数组中可以找到该局部变量的索引。每个条目必须包含以下五个项目。
start_pc, length
给定的局部变量在代码数组中的索引必须有一个值,该值在区间[start_pc, start_pc + length]内,也就是说,在start_pc(含)和start_pc + length(不含)之间。
start_pc的值必须是这个代码属性的代码数组中的有效索引,而且必须是一条指令的操作码的索引。
start_pc + length的值必须是这个代码属性的代码数组的有效索引,并且是一条指令的操作码的索引,或者它必须是该代码数组结束后的第一个索引。
名称_索引
name_index项的值必须是 constant_pool表的一个有效索引。该索引中的constant_pool条目必须包含一个CONSTANT_Utf8_info结构(§4.4.7),代表一个有效的表示局部变量的非限定名称(§4.2.2)。
描述符_索引
descriptor_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须包含一个CONSTANT_Utf8_info结构(§4.4.7),代表一个字段描述符,该描述符编码源程序中局部变量的类型(§4.3.2)。
指数
给定的局部变量必须在当前帧的局部变量阵列的索引处。
if索引处的局部变量是double或long类型的,它将同时占据索引和索引+1。
4.7.14.LocalVariableTypeTable属性
LocalVariableTypeTable属性是Code属性(§4.7.3)的属性表中的一个可选的变量长度属性。它可以被调试器用来在方法的执行过程中确定某个局部变量的值。
if多个LocalVariableTypeTable属性出现在一个给定的Code属性的属性表中,那么它们可以以任何顺序出现。
在一个代码属性的属性表中,每个局部变量不能超过一个LocalVariableTypeTable属性。
LocalVariableTypeTable属性与LocalVariableTable属性(§4.7.13)不同,它提供了签名信息而不是描述符信息。这个区别只对那些类型使用了类型变量或参数化类型的变量有意义。这样的变量将出现在两个表中,而其他类型的变量将只出现在LocalVariableTable中。
LocalVariableTypeTable属性有以下格式。
本地变量类型表_属性 {
u2属性_名称_索引。
u4 attribute_length。
u2 local_variable_type_table_length。
{ u2 start_pc;
u2长度。
u2 name_index。
u2 签名_索引。
u2指数。
} local_variable_type_table[local_variable_type_table_length]。
}
LocalVariableTypeTable_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是 constant_pool表的一个有效索引。该索引中的 constant_pool条目必须是一个 CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “LocalVariableTypeTable”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
本地_变量_类型_表长
local_variable_type_table_length项的值表示local_variable_type_table数组中的条目数量。
本地变量类型表[] 。
local_variable_type_table数组中的每个条目表示一个代码数组偏移量的范围,在这个范围内,局部变量有一个值。它还表示在当前帧的局部变量数组中可以找到该局部变量的索引。每个条目必须包含以下五个项目。
start_pc, length
给定的局部变量在代码数组中的索引必须有一个值,该值在区间[start_pc, start_pc + length]内,也就是说,在start_pc(含)和start_pc + length(不含)之间。
start_pc的值必须是这个代码属性的代码数组中的有效索引,而且必须是一条指令的操作码的索引。
start_pc + length的值必须是这个代码属性的代码数组的有效索引,并且是一条指令的操作码的索引,或者它必须是该代码数组结束后的第一个索引。
名称_索引
name_index项的值必须是 constant_pool表的一个有效索引。该索引中的constant_pool条目必须包含一个CONSTANT_Utf8_info结构(§4.4.7),代表一个有效的表示局部变量的非限定名称(§4.2.2)。
签名_索引
signature_index项的值必须是constant_pool表中的一个有效索引。该索引中的constant_pool条目必须包含一个CONSTANT_Utf8_info结构(§4.4.7),代表一个字段签名,该签名编码源程序中局部变量的类型(§4.7.9.1)。
指数
给定的局部变量必须在当前帧的局部变量阵列的索引处。
if索引处的局部变量是double或long类型的,它将同时占据索引和索引+1。
4.7.15.废弃的属性
Deprecated属性是 ClassFile、field_info或 method_info结构的属性表中的一个可选的固定长度属性(§4.1、§4.5、§4.6)。一个类、接口、方法或字段可以用Deprecated属性来标记,以表明该类、接口、方法或字段已被取代。
读取类文件格式的运行时解释器或工具,如编译器,可以使用这个标记来通知用户,一个被取代的类、接口、方法或字段正在被引用。废弃属性的存在并不改变一个类或接口的语义。
废弃的属性有以下格式。
废弃的属性 {
u2属性_名称_索引。
u4 attribute_length。
}
Deprecated_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是 constant_pool表的一个有效索引。该索引中的 constant_pool条目必须是一个 CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “Deprecated”。
属性_长度
attribute_length项的值必须为零。
4.7.16.RuntimeVisibleAnnotations属性
RuntimeVisibleAnnotations属性是 ClassFile、field_info或 method_info结构的属性表中的一个可变长度的属性(§4.1、§4.5、§4.6)。RuntimeVisibleAnnotations属性记录了对相应的类、字段或方法的声明的运行时可见注释。Java 虚拟机必须使这些注解可用,以便它们能够被适当的反射性 API 返回。
在ClassFile、field_info或method_info结构的属性表中,最多可以有一个RuntimeVisibleAnnotations属性。
RuntimeVisibleAnnotations属性有以下格式。
RuntimeVisibleAnnotations_attribute {
u2属性_名称_索引。
u4 attribute_length。
u2 num_annotations。
注释注释[num_annotations]。
}
RuntimeVisibleAnnotations_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是 constant_pool表的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “RuntimeVisibleAnnotations”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
num_annotations
num_annotations项的值给出了该结构所代表的运行时可见注释的数量。
注释[]
注释表中的每个条目都代表声明上的一个运行时可见注释。注释结构有以下格式。
注释 {
u2 type_index。
u2 num_element_value_pairs。
{ u2 element_name_index;
element_value值。
} element_value_pairs[num_element_value_pairs]。
}
注释结构的项目如下。
类型_索引
type_index项的值必须是 constant_pool表的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表一个字段描述符(§4.3.2)。字段描述符表示这个注释结构所代表的注释的类型。
num_element_value_pairs
num_element_value_pairs项的值给出了这个注解结构所代表的注解的元素-价值对的数量。
元素_值对[]
element_value_pairs表的每个值代表这个注解结构所代表的注解中的一个元素-价值对。每个element_value_pairs条目包含以下两个项目。
元素_名称_索引
element_name_index项的值必须是constant_pool表中的一个有效索引。该索引中的 constant_pool条目必须是一个 CONSTANT_Utf8_info结构(§4.4.7)。constant_pool条目表示这个element_value_pairs条目所代表的元素-值对的元素名称。
换句话说,该条目表示由type_index指定的注释类型的一个元素。
价值
值项的值代表这个 element_value_pairs条目所代表的元素-值对的值。
4.7.16.1.element_value结构
element_value结构是一个鉴别性的联合体,代表一个元素-价值对的价值。它有以下格式。
element_value {
u1标签。
联盟{
u2 const_value_index。
{ u2 type_name_index;
u2 const_name_index。
} enum_const_value;
u2 class_info_index。
注解 annotation_value。
{ u2 num_values;
element_value values[num_values]。
} array_value;
}值。
}
标签项使用一个ASCII字符来表示元素-值对的值的类型。这决定了使用的是值联盟中的哪一个项目。表4.7.16.1-A显示了标签项的有效字符,每个字符指示的类型,以及每个字符在值联盟中使用的项目。该表的第四列用于下面对值联盟的一个项目的描述。
表4.7.16.1-A.标签值作为类型的解释
标签项目 | 类型 | 价值项目 | 恒定类型 |
|---|---|---|---|
B | 字节 | 常设值_索引 | CONSTANT_Integer |
C | 炭 | 常设值_索引 | CONSTANT_Integer |
D | 双 | 常设值_索引 | CONSTANT_Double |
F | 浮动 | 常设值_索引 | CONSTANT_Float |
I | 䵮䵮 | 常设值_索引 | CONSTANT_Integer |
J | 长 | 常设值_索引 | CONSTANT_LONG |
S | 短期 | 常设值_索引 | CONSTANT_Integer |
Z | 布尔型 | 常设值_索引 | CONSTANT_Integer |
s | 字符串 | 常设值_索引 | CONSTANT_Utf8 |
e | 枚举类型 | enum_const_value | 不适用 |
c | 级别 | 类的信息_索引 | 不适用 |
@ | 注释类型 | 注释_价值 | 不适用 |
[ | 阵列类型 | array_value | 不适用 |
值项目表示一个元素-值对的值。该项是一个联合体,其自身的项目如下。
常设值_索引
const_value_index项表示一个原始的常量值或者一个Stringliteral作为这个元素-值对的值。
const_value_index项的值必须是constant_pool表中的一个有效索引。该索引中的constant_pool条目必须是适合标签项的类型,如表4.7.16.1-A的第四列所规定。
enum_const_value
enum_const_value项表示一个枚举常量作为这个元素-值对的值。
enum_const_value项目由以下两个项目组成。
类型_名称_索引
type_name_index项的值必须是 constant_pool表的一个有效索引。该索引中的 constant_pool条目必须是一个 CONSTANT_Utf8_info结构(§4.4.7),代表一个场描述符(§4.3.2)。constant_pool条目给出了这个 element_value结构所代表的枚举常量类型的二进制名称的内部形式(§4.2.1)。
命名为const_name_index
const_name_index项的值必须是constant_pool表中的一个有效索引。该索引下的 constant_pool条目必须是一个 CONSTANT_Utf8_info结构(§4.4.7)。constant_pool条目给出了这个 element_value结构所代表的枚举常量的简单名称。
类的信息_索引
class_info_index项表示一个类的字面意思,作为这个元素-值对的值。
class_info_index项必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表一个返回描述符(§4.3.3)。返回描述符给出了与这个 element_value结构所代表的类字头相对应的类型。类型与类字的对应关系如下。
· 对于一个类的字面意义C.class,其中C是一个类、接口或数组类型的名称,相应的类型是C,在constant_pool中的返回描述符将是一个ObjectType或ArrayType。
· 对于一个类的字面意义p.class,其中p是一个原始类型的名称,对应的类型是p,constant_pool中的返回描述符将是一个BaseType字符。
· 对于一个类的字面意思 void.class,对应的类型是 void。constant_pool中的返回描述符将是V。
例如,Class literal Object.class对应于Object类型,所以constant_pool条目是Ljava/lang/Object;,而Class literal int.class对应于int类型,所以constant_pool条目是I。
类的字面意义 void.class 对应于 void,所以 constant_pool条目是 V,而类的字面意义 Void.class 对应于 Void 类型,所以 constant_pool条目是 Ljava/lang/Void;。
注释_价值
annotation_value项表示一个 "嵌套 "的注释,作为这个元素-值对的值。
annotation_value项的值是一个注释结构(§4.7.16),它给出了这个元素_value结构所代表的注释。
array_value
array_value项表示一个数组作为这个元素-值对的值。
array_value项由以下两个项目组成。
num_values
num_values项的值给出了这个Element_value结构所代表的数组中的元素数量。
价值[]
值表中的每个值都给出了这个 element_value结构所代表的数组中的相应元素。
4.7.17.RuntimeInvisibleAnnotations属性
RuntimeInvisibleAnnotations属性是ClassFile、field_info或method_info结构(§4.1、§4.5、§4.6)的属性表中的一个可变长度的属性。RuntimeInvisibleAnnotations属性记录了相应的类、方法或字段的声明上的运行时不可见注释。
在ClassFile、field_info或method_info结构的属性表中,最多可以有一个RuntimeInvisibleAnnotations属性。
RuntimeInvisibleAnnotations属性与 RuntimeVisibleAnnotations属性(第 4.7.16 节)类似,但 RuntimeInvisibleAnnotations属性所代表的注解不得由反射性 API 返回,除非 Java 虚拟机被指示通过一些特定于实现的机制(如命令行标志)来保留这些注解。if没有这样的指示,Java 虚拟机将忽略这个属性。
RuntimeInvisibleAnnotations属性有以下格式。
RuntimeInvisibleAnnotations_attribute {
u2属性_名称_索引。
u4 attribute_length。
u2 num_annotations。
注释注释[num_annotations]。
}
RuntimeInvisibleAnnotations_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是 constant_pool表的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “RuntimeInvisibleAnnotations”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
num_annotations
num_annotations项的值给出了该结构所代表的运行时不可见注解的数量。
注释[]
注释表中的每个条目代表声明上的一个运行时不可见的注释。注释结构在§4.7.16中规定。
4.7.18.RuntimeVisibleParameterAnnotations属性
RuntimeVisibleParameterAnnotations属性是method_info结构(§4.6)的属性表中的一个可变长度的属性。RuntimeVisibleParameterAnnotations属性记录了对相应方法的形式参数声明的运行时可见注释。Java虚拟机必须使这些注解可用,以便它们可以被适当的反射性API返回。
在一个method_info结构的属性表中,最多可以有一个RuntimeVisibleParameterAnnotations属性。
RuntimeVisibleParameterAnnotations属性有以下格式。
RuntimeVisibleParameterAnnotations_attribute {
u2属性_名称_索引。
u4 attribute_length。
u1 num_parameters。
{ u2 num_annotations;
注释注释[num_annotations]。
} parameter_annotations[num_parameters]。
}
RuntimeVisibleParameterAnnotations_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是constant_pool表中的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “RuntimeVisibleParameterAnnotations”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
num_parameters
num_parameters项的值给出了发生注释的method_info结构所代表的方法的形式参数的数量。
这就重复了可以从方法描述符中提取的信息。
参数_注释[]
parameter_annotations表中的每个条目都代表了对单个形式参数声明的所有运行时可见注释。表中的第i条对应于方法描述符中的第i个形参(§4.3.3)。每个parameter_annotations条目包含以下两个项目。
num_annotations
num_annotations项的值表示参数_annotations项对应的形参声明上的运行时可见注释的数量。
注释[]
注释表中的每个条目都代表了对参数_注释条目所对应的形式参数声明的一个运行时可见注释。注释结构在§4.7.16中规定。
4.7.19.RuntimeInvisibleParameterAnnotations属性
RuntimeInvisibleParameterAnnotations属性是method_info结构的属性表中的一个可变长度的属性(§4.6)。RuntimeInvisibleParameterAnnotations属性记录了对相应方法的形式参数声明的运行时不可见注释。
在一个method_info结构的属性表中,最多可以有一个RuntimeInvisibleParameterAnnotations属性。
RuntimeInvisibleParameterAnnotations属性与 RuntimeVisibleParameterAnnotations属性(第 4.7.18 节)类似,但 RuntimeInvisibleParameterAnnotations属性所代表的注释不得由反射性 API 返回,除非 Java 虚拟机已被特别指示通过一些特定的实现机制(如命令行标志)来保留这些注释。if没有这样的指示,Java 虚拟机将忽略这个属性。
RuntimeInvisibleParameterAnnotations属性有以下格式。
RuntimeInvisibleParameterAnnotations_attribute {
u2属性_名称_索引。
u4 attribute_length。
u1 num_parameters。
{ u2 num_annotations;
注释注释[num_annotations]。
} parameter_annotations[num_parameters]。
}
RuntimeInvisibleParameterAnnotations_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “RuntimeInvisibleParameterAnnotations”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
num_parameters
num_parameters项的值给出了发生注释的method_info结构所代表的方法的形式参数的数量。
这就重复了可以从方法描述符中提取的信息。
参数_注释[]
parameter_annotations表中的每个条目都代表了对单个形式参数声明的所有运行时隐形注释。表中的第i条对应于方法描述符中的第i个形参(§4.3.3)。每个parameter_annotations条目包含以下两个项目。
num_annotations
num_annotations项的值表示参数_annotations项对应的形参声明上的运行时隐形注释的数量。
注释[]
注释表中的每个条目都代表了对参数_注释条目所对应的形式参数声明的一个运行时不可见注释。注释结构在§4.7.16中规定。
4.7.20.RuntimeVisibleTypeAnnotations属性
RuntimeVisibleTypeAnnotations属性是ClassFile、field_info或method_info结构的属性表中的一个变长属性,或Code属性(§4.1, §4.5, §4.6, §4.7.3)。RuntimeVisibleTypeAnnotations属性记录了在相应的类、字段或方法的声明中,或在相应的方法体的表达式中使用的类型的运行时可见注释。RuntimeVisibleTypeAnnotations属性还记录了对通用类、接口、方法和构造函数的类型参数声明的运行时可见注释。Java 虚拟机必须使这些注解可用,以便它们可以被适当的反射性 API 返回。
在ClassFile、field_info或method_info结构的属性表中,最多可以有一个RuntimeVisibleTypeAnnotations属性,或Code属性。
只有当类型被注解在与属性表的父结构或属性相对应的声明或表达式的种类中时,属性表才包含 RuntimeVisibleTypeAnnotations 属性。
例如,在类声明的 implements子句中对类型的所有注释都记录在类的 ClassFile结构的 RuntimeVisibleTypeAnnotations属性中。同时,字段声明中关于类型的所有注释都记录在字段的field_info结构的RuntimeVisibleTypeAnnotations属性中。
RuntimeVisibleTypeAnnotations属性有以下格式。
RuntimeVisibleTypeAnnotations_attribute {
u2属性_名称_索引。
u4 attribute_length。
u2 num_annotations。
type_annotation annotations[num_annotations]。
}
RuntimeVisibleTypeAnnotations_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是constant_pool表中的一个有效索引。该索引中的constant_pool条目必须是一个CONSTANT_Utf8_info结构,代表字符串 “RuntimeVisibleTypeAnnotations”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
num_annotations
num_annotations项的值给出了该结构所代表的运行时可见类型注释的数量。
注释[]
注释表中的每个条目代表在声明或表达式中使用的类型的一个运行时可见注释。type_annotation结构有如下格式。
type_annotation {
u1 target_type;
联盟{
type_parameter_target。
supertype_target。
type_parameter_bound_target。
empty_target。
method_formal_parameter_target。
throws_target;
localvar_target。
catch_target。
offset_target。
type_argument_target。
} target_info;
type_path target_path。
u2 type_index。
u2 num_element_value_pairs。
{ u2 element_name_index;
element_value值。
} element_value_pairs[num_element_value_pairs]。
}
前三个项目–target_type、target_info和target_path–指定被注释的类型的精确位置。最后三个项目–type_index、num_element_value_pairs和element_value_pairs[]–指定注释自己的类型和元素-价值对。
type_annotation结构的项目如下。
目标_类型
target_type项的值表示注释出现在哪种目标上。各种类型的目标对应于Java编程语言的类型上下文,其中类型被用在声明和表达式中(JLS §4.11)。
target_type的合法值在表4.7.20-A和表4.7.20-B中规定。每个值都是一个单字节的标签,表明target_info联盟中的哪一项紧随target_type项,以提供关于目标的更多信息。
表4.7.20-A和表4.7.20-B中的目标种类对应于JLS §4.11中的类型上下文。也就是说,target_type值0x10-0x17和0x40-0x42对应于类型上下文1-10,而target_type值0x43-0x4B对应于类型上下文11-16。
target_type项的值决定了 type_annotation结构是否出现在 ClassFile结构、field_info结构、method_info结构或 Code属性的 RuntimeVisibleTypeAnnotations属性中。表 4.7.20-C 给出了具有每个合法 target_type值的 type_annotation结构的 RuntimeVisibleTypeAnnotations属性的位置。
目标信息
target_info项的值精确地表示声明或表达式中的哪个类型被注释。
target_info联盟的项目在§4.7.20.1中规定。
target_path
target_path项的值精确地表示了由target_info指示的类型的哪一部分被注释。
type_path结构的格式在§4.7.20.2中规定。
type_index, num_element_value_pairs, element_value_pairs[]
这些项目在type_annotation结构中的含义与它们在annotation结构中的含义相同(§4.7.16)。
表4.7.20-A.target_type值的解释(第1部分)
| 价值 | 目标的种类 | 目标信息项目 |
|---|---|---|
| 0x00 | 通用类或接口的类型参数声明 | type_parameter_target |
| 0x01 | 通用方法或构造函数的类型参数声明 | type_parameter_target |
| 0x10 | 类型在类声明的扩展或实现子句中(包括匿名类声明的直接超类或直接超面),或在接口声明的扩展子句中 | Supertype_target |
| 0x11 | 绑定泛型类或接口的类型参数声明中的类型 | type_parameter_bound_target |
| 0x12 | 绑定泛型方法或构造函数的类型参数声明中的类型 | type_parameter_bound_target |
| 0x13 | 字段声明中的类型 | 空目标 |
| 0x14 | 方法的返回类型,或新构建对象的类型 | 空目标 |
| 0x15 | 方法或构造器的接收类型 | 空目标 |
| 0x16 | 方法、构造函数或lambda表达式的正式参数声明中的类型 | 形态参数目标 |
| 0x17 | 方法或构造函数的抛出句中的类型 | 抛出的目标 |
表4.7.20-B.target_type值的解释(第2部分)
| 价值 | 目标的种类 | 目标信息项目 |
|---|---|---|
| 0x40 | 本地变量声明中的类型 | 本地var_target |
| 0x41 | 资源变量声明中的类型 | 本地var_target |
| 0x42 | 异常参数声明中的类型 | 捕捉目标(catch_target |
| 0x43 | 表达式实例中的类型 | 偏移量_target |
| 0x44 | 输入新的表达方式 | 偏移量_target |
| 0x45 | 在方法引用表达式中使用::new | 偏移量_target |
| 0x46 | 使用::标识符在方法参考表达式中输入类型 | 偏移量_target |
| 0x47 | 在投射表达中的类型 | type_argument_target |
| 0x48 | 新表达式或显式构造函数调用语句中通用构造函数的类型参数 | type_argument_target |
| 0x49 | 方法调用表达式中通用方法的类型参数 | type_argument_target |
| 0x4A | 使用::new的方法引用表达式中的通用构造函数的类型参数 | type_argument_target |
| 0x4B | 在方法引用表达式中使用::标识符的通用方法的类型参数 | type_argument_target |
表4.7.20-C.target_type值的包围属性的位置
| 价值 | 目标的种类 | 地点 |
|---|---|---|
| 0x00 | 通用类或接口的类型参数声明 | 类文件 |
| 0x01 | 通用方法或构造函数的类型参数声明 | Method_info |
| 0x10 | 在类或接口声明的扩展子句中的类型,或在接口声明的实现子句中的类型。 | 类文件 |
| 0x11 | 绑定泛型类或接口的类型参数声明中的类型 | 类文件 |
| 0x12 | 绑定泛型方法或构造函数的类型参数声明中的类型 | Method_info |
| 0x13 | 字段声明中的类型 | 栏位_信息 |
| 0x14 | 方法或构造函数的返回类型 | Method_info |
| 0x15 | 方法或构造器的接收类型 | Method_info |
| 0x16 | 方法、构造函数或lambda表达式的正式参数声明中的类型 | Method_info |
| 0x17 | 方法或构造函数的抛出句中的类型 | Method_info |
| 0x40-0x4B | 本地变量声明、资源变量声明、异常参数声明、表达式中的类型 | 编码 |
4.7.20.1.target_info联盟
target_info联盟的项目(除了第一个)精确地指定了声明或表达式中的哪个类型被注释。第一个项目不是指定哪个类型,而是指定哪个类型参数的声明被注解。这些项目如下。
· type_parameter_target项表示注解出现在泛型类、泛型接口、泛型方法或泛型构造器的第i个类型参数的声明中。
· type_parameter_target {
· u1 type_parameter_index。
· }
type_parameter_index项的值指定哪个类型参数声明被注释。type_parameter_index的值为0,指定第一个类型参数声明。
· supertype_target项表示注释出现在类或接口声明的extends或 implements子句中的一个类型上。
· supertype_target {
· u2 supertype_index。
· }
supertype_index值为65535,指定注释出现在类声明的extends子句中的超类上。
任何其它的 supertype_index值都是包围 ClassFile结构的接口数组中的一个索引,并且指定注释出现在类声明的 implements子句或接口声明的 extends子句中的那个超接口上。
· type_parameter_bound_target项表示一个注解出现在泛型类、接口、方法或构造函数的第j个类型参数声明的第i个边界。
· type_parameter_bound_target {
· u1 type_parameter_index。
· u1 bound_index。
· }
type_parameter_index项的值指定了哪个类型的参数声明有一个注释的约束。type_parameter_index的值为0,指定第一个类型参数声明。
bound_index项的值指定由type_parameter_index指示的类型参数声明的哪个边界被注释。bound_index的值为0,指定了类型参数声明的第一个边界。
type_parameter_bound_target项记录了一个绑定被注释了,但是没有记录构成该绑定的类型。该类型可以通过检查存储在适当的Signature属性中的类签名或方法签名来找到。
· empty_target项表示注释出现在字段声明中的类型、方法的返回类型、新构造的对象的类型、方法或构造函数的接收类型。
· empty_target {
· }
在这些地方只有一个类型出现,所以在target_info联盟中没有每个类型的信息可以表示。
· formal_parameter_target项表示注释出现在方法、构造函数或lambda表达式的形式参数声明中的类型上。
· 正式参数目标 {
· u1 formal_parameter_index。
· }
formal_parameter_index项的值指定了哪个正式参数声明有一个注释的类型。formal_parameter_index值为0时,指定第一个正式参数声明。
formal_parameter_target项记录了一个形式参数的类型被注释了,但并没有记录类型本身。该类型可以通过检查方法描述符(§4.3.3)的方法_info结构中的RuntimeVisibleTypeAnnotations属性找到。formal_parameter_index值为0表示方法描述符中的第一个参数描述符。
· throws_target项表示注释出现在方法或构造函数声明的throws子句中的第i个类型上。
· 抛出的目标 {
· u2 throws_type_index。
· }
throws_type_index项的值是包围RuntimeVisibleTypeAnnotations属性的method_info结构的Exceptions属性的exception_index_table数组中的一个索引。
· localvar_target项表示注释出现在局部变量声明的类型上,包括在try-with-resources语句中作为资源声明的变量。
· localvar_target {
· u2 table_length。
· { u2 start_pc;
· u2长度。
· u2指数。
· } table[table_length]。
· }
table_length项的值给出了表阵列中的条目数。每个条目表示一个代码数组的偏移量范围,在这个范围内,局部变量有一个值。它还表示在当前帧的局部变量数组中可以找到该局部变量的索引。每个条目包含以下三个项目。
start_pc, length
给定的局部变量在代码数组中的索引处有一个值,位于[start_pc, start_pc + length]的区间内,也就是说,在start_pc(包括)和start_pc + length(不包括)之间。
指数
给定的局部变量必须在当前帧的局部变量阵列的索引处。
if索引处的局部变量是double或long类型的,它将同时占据索引和索引+1。
需要一个表来完全指定其类型被注释的局部变量,因为一个局部变量可以在多个活范围内用不同的局部变量索引来表示。每个表项中的start_pc、length和index项指定的信息与LocalVariableTable属性相同。
localvar_target项记录了一个局部变量的类型被注释了,但并没有记录类型本身。该类型可以通过检查适当的LocalVariableTable属性来找到。
· catch_target项表示注释出现在异常参数声明中的第i个类型上。
· catch_target {
· u2 exception_table_index。
· }
exception_table_index项的值是围着RuntimeVisibleTypeAnnotations属性的Code属性的exception_table数组的索引。
异常参数声明中出现多个类型的可能性来自于 try 语句的多抓取子句,在这个子句中,异常参数的类型是一个类型的联合(JLS §14.20)。编译器通常为联合体中的每个类型创建一个 exception_table条目,这使得 catch_target项可以区分它们。这保留了一个类型和它的注解之间的对应关系。
· offset_target项表示注释出现在instanceof表达式或新表达式中的类型上,或者出现在方法引用表达式中::之前的类型。
· offset_target {
· u2偏移。
· }
offset项的值指定与instanceof表达式对应的instanceof字节码指令、与新表达式对应的新字节码指令或与方法引用表达式对应的字节码指令的代码数组偏移。
· type_argument_target项表示注解出现在 cast 表达式中的第 i 个类型上,或者出现在以下任何一个显式类型参数列表中的第 i 个类型参数上:新表达式、显式构造函数调用语句、方法调用表达式或方法引用表达式。
· type_argument_target {
· u2偏移。
· u1 type_argument_index。
· }
offset项的值指定了与cast表达式对应的字节码指令、与新表达式对应的新字节码指令、与显式构造函数调用语句对应的字节码指令、与方法调用表达式对应的字节码指令或与方法引用表达式对应的字节码指令的代码阵列偏移。
对于一个转换表达式,type_argument_index项的值指定转换操作符中的哪个类型被注释。type_argument_index的值是0,它指定了投掷运算符中的第一个(或唯一)类型。
在一个投射表达式中出现一个以上的类型的可能性来自于投射到一个交叉类型。
对于一个明确的类型参数列表,type_argument_index项的值指定哪个类型参数被注释。type_argument_index值为0时,指定第一个类型参数。
4.7.20.2.type_path结构
只要在声明或表达式中使用了一个类型,type_path结构就会确定该类型的哪一部分被注释了。注释可以出现在类型本身上,但是if类型是一个引用类型,那么还有其他可以出现注释的位置。
· if在声明或表达式中使用了一个数组类型T[],那么注释可以出现在数组类型的任何组件类型上,包括元素类型。
· if在声明或表达式中使用了嵌套类型T1.T2,那么注释可以出现在顶层类型的名称或任何成员类型上。
· if在声明或表达式中使用了参数化类型 T<A>或 T<? extends A>或 T<? super A>,那么注释可以出现在任何类型参数或任何通配符类型参数的边界上。
例如,考虑到String[][]的不同部分被注解在。
@Foo String[][] // 注释了类的类型 String
String @Foo [][] // 注释数组类型 String[][] 。
String[] @Foo [] // 注释了数组类型 String[] 。
或嵌套类型Outer.Middle.Inner的不同部分被注解在。
@Foo Outer.Middle.Inner
外部.@Foo 中间.内部
外部.中间.@Foo 内部
或参数化类型Map<String,Object>和List<...>的不同部分被注释进去。
@Foo Map<String,Object>
地图<@Foo 字符串,对象
地图<String,@Foo Object>
列表<@Foo ? 扩展为字符串>。
列表<? extends @Foo String>。
type_path结构有如下格式。
type_path {
u1 path_length。
{ u1 type_path_kind;
u1 type_argument_index。
} path[path_length]。
}
path_length项的值给出了路径数组中的条目数。
· ifpath_length的值是0,那么注释就直接出现在类型本身。
· ifpath_length的值为非零,那么路径数组中的每一个条目都代表一个迭代的、从左到右的步骤,指向数组类型、嵌套类型或参数化类型中注释的精确位置。(在一个数组类型中,迭代访问数组类型本身,然后是它的组件类型,然后是该组件类型的组件类型,以此类推,直到到达元素类型)。每个条目都包含以下两个项目。
type_path_kind
type_path_kind项目的合法值在表4.7.20.2-A中列出。
表4.7.20.2-A.type_path_kind值的解释
| 价值 | 解释 |
|---|---|
0 | 注释在一个数组类型中更深入 |
1 | 注释在一个嵌套的类型中更加深入 |
2 | 注释在一个参数化类型的通配符类型参数的边界上 |
3 | 注释在一个参数化的类型参数上 |
Type_argument_index
iftype_path_kind项的值是0,1,或者2,那么type_argument_index项的值就是0。
iftype_path_kind项的值是3,那么type_argument_index项的值指定了一个参数化类型的哪个类型参数被注释,其中0表示参数化类型的第一个类型参数。
表4.7.20.2-B.type_path结构,用于@A Map<@B ? extends @C String, @D List<@E Object>>。
| 注释 | 路径_长度 | 路 |
|---|---|---|
@A | 0 | [] |
@B | 1 | [{type_path_kind: 3; type_argument_index: 0}] |
@C | 2 | [{type_path_kind: 3; type_argument_index:0}, {type_path_kind: 2; type_argument_index:0}] |
@D | 1 | [{type_path_kind: 3; type_argument_index: 1}] |
@E | 2 | [{type_path_kind: 3; type_argument_index:1}, {type_path_kind: 3; type_argument_index:0}] |
表4.7.20.2-C. @I 字符串的type_path结构 @F [] @G [] @H []。
| 注释 | 路径_长度 | 路 |
|---|---|---|
@F | 0 | [] |
@G | 1 | [{type_path_kind: 0; type_argument_index: 0}] |
@H | 2 | [{type_path_kind: 0; type_argument_index:0}, {type_path_kind: 0; type_argument_index:0}] |
@I | 3 | [{type_path_kind: 0; type_argument_index:0}, {type_path_kind: 0; type_argument_index:0}, {type_path_kind: 0; type_argument_index:0}] |
表4.7.20.2-D.@A 列表<@B 可比<@F 对象@C [] @D [] @E []>的type_path结构
| 注释 | 路径_长度 | 路 |
|---|---|---|
@A | 0 | [] |
@B | 1 | [{type_path_kind: 3; type_argument_index: 0}] |
@C | 2 | [{type_path_kind: 3; type_argument_index:0}, {type_path_kind: 3; type_argument_index:0}] |
@D | 3 | [{type_path_kind: 3; type_argument_index:0}, {type_path_kind: 3; type_argument_index:0}, {type_path_kind: 0; type_argument_index:0}] |
@E | 4 | [{type_path_kind: 3; type_argument_index:0}, {type_path_kind: 3; type_argument_index:0}, {type_path_kind: 0; type_argument_index:0}, {type_path_kind: 0; type_argument_index:0}] |
@F | 5 | [{type_path_kind: 3; type_argument_index:0}, {type_path_kind: 3; type_argument_index:0}, {type_path_kind: 0; type_argument_index:0}, {type_path_kind: 0; type_argument_index:0}, {type_path_kind: 0; type_argument_index:0}] |
表4.7.20.2-E.type_path结构,用于@C外层.@B 中间.@A 内部
| 注释 | 路径_长度 | 路 |
|---|---|---|
@A | 2 | [{type_path_kind: 1; type_argument_index:0}, {type_path_kind: 1; type_argument_index:0}] |
@B | 1 | [{type_path_kind: 1; type_argument_index: 0}] |
@C | 0 | [] |
表4.7.20.2-F. Outer的type_path结构。中间<@D Foo .@C Bar> .内部<@B 字符串 @A []>。
| 注释 | 路径_长度 | 路 |
|---|---|---|
@A | 3 | [{type_path_kind: 1; type_argument_index:0}, {type_path_kind: 1; type_argument_index:0}, {type_path_kind: 3; type_argument_index:0}] |
@B | 4 | [{type_path_kind: 1; type_argument_index:0}, {type_path_kind: 1; type_argument_index:0}, {type_path_kind: 3; type_argument_index:0}, {type_path_kind: 0; type_argument_index:0}] |
@C | 3 | [{type_path_kind: 1; type_argument_index:0}, {type_path_kind: 3; type_argument_index:0}, {type_path_kind: 1; type_argument_index:0}] |
@D | 2 | [{type_path_kind: 1; type_argument_index:0}, {type_path_kind: 3; type_argument_index:0}] |
4.7.21.RuntimeInvisibleTypeAnnotations属性
RuntimeInvisibleTypeAnnotations属性是ClassFile、field_info或method_info结构的属性表中的一个可变长度的属性,或Code属性(§4.1, §4.5, §4.6, §4.7.3)。RuntimeInvisibleTypeAnnotations属性记录了在类、字段或方法的相应声明中或在相应方法体的表达式中使用的类型的运行时不可见注释。RuntimeInvisibleTypeAnnotations属性还记录了对泛型类、接口、方法和构造函数的类型参数声明的注释。
在ClassFile、field_info或method_info结构的属性表中,最多可以有一个RuntimeInvisibleTypeAnnotations属性,或者Code属性。
只有当类型被注解在与属性表的父结构或属性相对应的声明或表达式的种类中时,属性表才包含 RuntimeInvisibleTypeAnnotations属性。
RuntimeInvisibleTypeAnnotations属性有以下格式。
RuntimeInvisibleTypeAnnotations_attribute {
u2属性_名称_索引。
u4 attribute_length。
u2 num_annotations。
type_annotation annotations[num_annotations]。
}
RuntimeInvisibleTypeAnnotations_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构,代表字符串 “RuntimeInvisibleTypeAnnotations”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
num_annotations
num_annotations项的值给出了该结构所代表的运行时不可见类型注释的数量。
注释[]
注释表中的每个条目代表声明或表达式中使用的类型上的一个运行时不可见的注释。type_annotation结构在§4.7.20中指定。
4.7.22.AnnotationDefault属性
AnnotationDefault属性是某些method_info结构(§4.6)的属性表中的一个可变长度的属性,即那些代表注释类型的元素(JLS §9.6.1)。AnnotationDefault属性记录了method_info结构所代表的元素的默认值(JLS §9.6.2)。Java虚拟机必须使这个默认值可用,这样它就可以被适当的反射性API应用。
在 method_info结构的属性表中最多可以有一个 AnnotationDefault属性,该属性代表一个注释类型的元素。
AnnotationDefault属性有以下格式。
注释Default_attribute {
u2属性_名称_索引。
u4 attribute_length。
element_value default_value。
}
AnnotationDefault_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “AnnotationDefault”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
default_value
default_value项表示由包围这个AnnotationDefault属性的method_info结构所代表的注释类型元素的默认值。
4.7.23.BootstrapMethods属性
BootstrapMethods属性是 ClassFile结构(§4.1)的属性表中的一个可变长度的属性。BootstrapMethods属性记录了由invokedynamic指令(§invokedynamic)引用的bootstrap方法指定器。
if ClassFile 结构的 constant_pool表至少有一个 CONSTANT_InvokeDynamic_info条目,那么在该结构的属性表中必须有一个 BootstrapMethods属性 (§4.4.10)。
在 ClassFile结构的属性表中,最多可以有一个 BootstrapMethods属性。
BootstrapMethods属性有以下格式。
BootstrapMethods_attribute {
u2属性_名称_索引。
u4 attribute_length。
u2 num_bootstrap_methods。
{ u2 bootstrap_method_ref;
u2 num_bootstrap_arguments。
u2 bootstrap_arguments[num_bootstrap_arguments]。
} bootstrap_methods[num_bootstrap_methods]。
}
BootstrapMethods_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构(§4.4.7),代表字符串 “BootstrapMethods”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
因此,attribute_length项的值取决于这个ClassFile结构中\被调用的动态\指令的数量。
num_bootstrap_methods
num_bootstrap_methods项的值决定了bootstrap_methods数组中bootstrap方法指定器的数量。
引导方法[]
bootstrap_methods表中的每个条目都包含一个指向CONSTANT_MethodHandle_info结构(§4.4.8)的索引,该结构指定了一个bootstrap方法,以及一个指向bootstrap方法的静态参数的序列(可能是空的)索引。
每个bootstrap_methods条目必须包含以下三项内容。
采集方法参考
bootstrap_method_ref项的值必须是constant_pool表中的一个有效索引。该索引中的 constant_pool条目必须是 CONSTANT_MethodHandle_info结构(§4.4.8)。
方法句柄的形式是由§\invokedynamic中的调用站点指定符驱动的,在java.lang.invoke.MethodHandle中执行invoke要求引导方法句柄可以调整为实际传递的参数,就像调用java.lang.invoke.MethodHandle.asType一样。因此,CONSTANT_MethodHandle_info结构的reference_kind项应该有6或8的值(§5.4.3.5),并且reference_index项应该指定一个静态方法或构造函数,它需要三个参数,依次是java.lang.invoke.MethodHandles.Lookup、String和java.lang.invoke.MethodType。否则,在调用站点指定器的解析过程中,对引导方法句柄的调用将突然完成。
num_bootstrap_arguments
num_bootstrap_arguments项的值给出了bootstrap_arguments数组中的项目数量。
bootstrap_arguments[]
bootstrap_arguments数组中的每个条目必须是constant_pool表中的一个有效索引。该索引中的constant_pool条目必须是CONSTANT_String_info、CONSTANT_Class_info、CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_MethodHandle_info或CONSTANT_MethodType_info结构(§4.4.3, §4.4.1, §4.4.4, §4.4.5, §4.4.8, §4.4.9)。
4.7.24.MethodParameters属性
MethodParameters属性是method_info结构(§4.6)的属性表中的一个可变长度的属性。MethodParameters属性记录了关于一个方法的正式参数的信息,比如它们的名字。
在method_info结构的属性表中,最多可以有一个MethodParameters属性。
MethodParameters属性有以下格式。
MethodParameters_attribute {
u2属性_名称_索引。
u4 attribute_length。
u1 parameters_count。
{ u2 name_index;
u2 access_flags。
} 参数[parameters_count]。
}
MethodParameters_attribute结构的项目如下。
属性名_索引
attribute_name_index项的值必须是constant_pool表中的一个有效索引。该索引的constant_pool条目必须是一个CONSTANT_Utf8_info结构,代表字符串 “MethodParameters”。
属性_长度
attribute_length项的值表示属性的长度,不包括最初的六个字节。
参数_count
parameters_count项的值表示方法描述符(§4.3.3)中的参数描述符的数量,这些参数描述符由该属性所包围的method_info结构的descriptor_index引用。
这不是Java虚拟机实现在格式检查时必须执行的约束(§4.8)。方法描述符中的参数描述符与下面的参数数组中的项目相匹配的任务由Java SE平台的反射库完成。
参数[]
参数数组中的每个条目都包含以下一对项目。
名称_索引
name_index项的值必须是0或者是 constant_pool表中的有效索引。
ifname_index项的值为0,那么这个参数元素表示一个没有名字的正式参数。
ifname_index项的值为非零,那么该索引处的constant_pool条目必须是一个CONSTANT_Utf8_info结构,代表一个有效的表示形式参数的非限定名称(§4.2.2)。
访问_flags
access_flags项的值如下。
0x0010 (ACC_FINAL)
表示该形式参数被宣布为最终参数。
0x1000 (ACC_SYNTHETIC)
表示根据编写源代码的语言规范,形式参数没有在源代码中明确或隐含的声明(JLS §13.1)。(形式参数是产生这个类文件的编译器的一个实现工件)。
0x8000 (ACC_MANDATED)
表示形式参数在源代码中被隐式声明,根据编写源代码的语言规范(JLS §13.1)。(形式参数是由语言规范规定的,所以该语言的所有编译器都必须发出它)。
参数数组中的第i个条目对应于包围方法的描述符中的第i个参数描述符。(parameters_count项是一个字节,因为一个方法描述符只能有255个参数)。实际上,这意味着参数数组存储了该方法的所有参数信息。我们可以想象其他的方案,在参数数组中的条目指定其相应的参数描述符,但这将使MethodParameters属性变得过于复杂。
参数数组中的第i个条目可能与包围方法的Signature属性(if存在)中的第i个类型相对应,也可能与包围方法的参数注解中的第i个注解不一致。
4.8.格式检查
当一个潜在的类文件被Java虚拟机加载时(§5.3),Java虚拟机首先确保该文件具有类文件的基本格式(§4.1)。这个过程被称为格式检查。检查的内容如下。
· 前四个字节必须包含正确的魔法数字。
· 所有公认的属性必须具有适当的长度。
· 类文件不能被截断,也不能在末尾有多余的字节。
· 常量池必须满足§4.4中记载的约束条件。
例如,常量池中的每个CONSTANT_Class_info结构必须在其name_index项中包含一个有效的CONSTANT_Utf8_info结构的常量池索引。
· 常量池中的所有字段引用和方法引用必须有有效的名称、有效的类和有效的描述符(§4.3)。
格式检查并不确保给定的字段或方法确实存在于给定的类中,也不确保给定的描述符指的是真正的类。格式检查只确保这些项目的形式良好。更详细的检查是在验证字节码本身和解析过程中进行的。
这些对基本类文件完整性的检查对于任何对类文件内容的解释都是必要的。格式检查与字节码验证不同,尽管历史上它们被混淆了,因为两者都是完整性检查的一种形式。
4.9.对Java虚拟机代码的约束
方法、实例初始化方法、类或接口初始化方法(§2.9)的代码被存储在类文件的 method_info结构的 Code属性的代码数组中(§4.7.3)。本节描述了与Code_attribute结构的内容相关的约束。
4.9.1.静态约束
对一个类文件的静态约束是那些定义文件的良好形式的约束。这些约束已经在前面的章节中给出,除了对类文件中代码的静态约束。对类文件中的代码的静态约束规定了Java虚拟机指令必须如何在代码阵列中布局,以及各个指令的操作数必须是什么。
代码阵列中的指令的静态约束如下。
· 只有§6.5中记载的指令实例可以出现在代码阵列中。使用保留操作码(§6.2)的指令实例或本规范中没有记载的任何操作码不得出现在代码阵列中。
if类文件的版本号是51.0或以上,那么jsr操作码和jsr_w操作码都不可能出现在代码阵列中。
· 代码阵列中第一条指令的操作码从索引0开始。
· 对于代码数组中的每条指令,除了最后一条,下一条指令的操作码索引等于当前指令的操作码索引加上该指令的长度,包括其所有操作数。
为了这些目的,宽指令和其他指令一样;指定宽指令要修改的操作的操作码被视为该宽指令的一个操作数。该操作码决不能直接被计算所触及。
· 代码阵列中最后一条指令的最后一个字节必须是索引code_length-1的字节。
代码阵列中指令的操作数的静态约束如下。
· 每条跳转和分支指令(jsr, jsr_w, goto, goto_w, ifeq, ifne, ifle, iflt, ifge, ifgt, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmple, if_icmplt, if_icmpge, if_icmpgt, if_acmpeq, if_acmpne)的目标必须是这个方法中指令的操作码。
跳转或分支指令的目标绝不能是用于指定宽指令修改的操作码;跳转或分支的目标可以是宽指令本身。
· 每条表开关指令的目标(包括默认)必须是这个方法中的指令的操作码。
每条表开关指令在其跳转表中的条目数必须与它的低位和高位跳转表操作数的值一致,其低位值必须小于或等于其高位值。
表转换指令的目标不能是用于指定宽指令修改的操作的操作码;表转换的目标可以是宽指令本身。
· 每条lookupswitch指令的目标(包括缺省)必须是这个方法中的指令的操作码。
每条lookupswitch指令必须有一定数量的匹配-偏移对,与它的npairs操作数的值一致。匹配-偏移对必须按有符号的匹配值以递增的数字顺序进行排序。
Lookupswitch指令的目标不能是用于指定宽指令修改的操作码;lookupswitch的目标可以是一个宽指令本身。
· 每条ldc指令和ldc_w指令的操作数必须是constant_pool表中的一个有效索引。该索引所引用的常量池条目必须是类型。
o CONSTANT_Integer, CONSTANT_Float, 或 CONSTANT_String,if类文件的版本号小于49.0。
o CONSTANT_Integer, CONSTANT_Float, CONSTANT_String, 或者if类文件的版本号是49.0或50.0,则为CONSTANT_Class。
o CONSTANT_Integer, CONSTANT_Float, CONSTANT_String, CONSTANT_Class, CONSTANT_MethodType, 或 CONSTANT_MethodHandle,if类文件的版本号是51.0或以上。
· 每条ldc2_w指令的操作数必须代表 constant_pool表中的一个有效索引。该索引所引用的常量池条目必须是CONSTANT_Long或CONSTANT_Double类型。
后续的常量池索引也必须是常量池的有效索引,并且该索引处的常量池条目不能被使用。
· 每条getfield, putfield, getstatic, 和putstatic指令的操作数必须代表constant_pool表中的一个有效索引。该索引所引用的常量池条目必须是CONSTANT_Fieldref类型。
· 每条invokevirtual指令的indexbyte操作数必须代表 constant_pool表中的一个有效索引。该索引所引用的常量池条目必须是CONSTANT_Methodref类型。
· 每条invokespecial和invokestatic指令的indexbyte操作数必须代表 constant_pool表中的有效索引。if类文件的版本号小于52.0,该索引所引用的常量池条目必须是CONSTANT_Methodref类型;if类文件的版本号是52.0或以上,该索引所引用的常量池条目必须是CONSTANT_Methodref或CONSTANT_InterfaceMethodref类型。
· 每条invokeinterface指令的indexbyte操作数必须代表 constant_pool表中的一个有效索引。该索引所引用的常量池条目必须是CONSTANT_InterfaceMethodref类型。
每条invokeinterface指令的count操作数必须反映存储传递给接口方法的参数所需的局部变量的数量,这是由CONSTANT_InterfaceMethodref常量池条目引用的CONSTANT_NameAndType_info结构的描述符暗示的。
每条invokeinterface指令的第四个操作字节的值必须是0。
· 每条invokedynamic指令的indexbyte操作数必须代表 constant_pool表中的一个有效索引。该索引所引用的常量池条目必须是CONSTANT_InvokeDynamic类型。
每条被调用的动态指令的第三和第四个操作数字节的值必须为零。
· 只有invokespecial指令被允许调用一个实例初始化方法(§2.9)。
其他名称以'<’(’\u003c’)开头的方法都不能被方法调用指令调用。特别是,特别命名为<clinit>的类或接口初始化方法永远不会被Java虚拟机指令显式调用,而只能由Java虚拟机本身隐式调用。
· 每条instanceof、checkcast、new和anewarray指令的操作数,以及每条multianewarray指令的indexbyte操作数,必须代表constant_pool表中的一个有效索引。该索引所引用的常量池条目必须是CONSTANT_Class类型。
· 任何新指令都不能引用代表数组类型的CONSTANT_Class类型的常量池条目(§4.3.2)。新指令不能用来创建一个数组。
· 不能用anewarray指令来创建超过255维的数组。
· 多重数组指令只能用于创建一个至少与它的维数操作数一样多的数组类型。也就是说,虽然多重数组指令不需要创建其indexbyte操作数所引用的数组类型的所有维数,但是它不能试图创建比数组类型中更多的维数。
每条多重数组指令的尺寸操作数不得为零。
· 每条newarray指令的atype操作数必须是T_BOOLEAN(4), T_CHAR(5), T_FLOAT(6), T_DOUBLE(7), T_BYTE(8), T_SHORT(9), T_INT(10), 或T_LONG(11)其中之一。
· 每条iload, fload, aload, istore, fstore, astore, iinc和ret指令的索引操作数必须是一个不大于max_locals-1的非负整数。
每条iload_, fload_, aload_, istore_, fstore_, and astore_指令的隐含索引必须不大于max_locals - 1。
· 每个lload、dload、lstore和dstore指令的索引操作数不得大于max_locals - 2。
每个lload_、dload_、lstore_和dstore_指令的隐含索引必须不大于max_locals - 2。
· 修改iload, fload, aload, istore, fstore, astore, iinc或ret指令的每个wide指令的indexbyte操作数必须代表一个不大于max_locals-1的非负整数。
修改lload、dload、lstore或dstore指令的每个wide指令的indexbyte操作数必须代表一个不大于max_locals-2的非负整数。
4.9.2.结构性制约因素
代码阵列上的结构性约束规定了对Java虚拟机指令之间关系的约束。结构约束如下。
· 每条指令都必须在操作数栈和局部变量数组中有适当类型和数量的参数的情况下执行,无论导致其调用的执行路径如何。
对int类型的值进行操作的指令也被允许对boolean、byte、char和short类型的值进行操作。
正如第2.3.4节和第2.11.1节所指出的,Java虚拟机在内部将布尔、字节、短和char类型的值转换为int类型。)
· if一条指令可以沿着几个不同的执行路径执行,那么在执行指令之前,无论采取何种路径,操作数栈必须具有相同的深度(§2.6.2)。
· 在执行过程中,任何时候操作数栈的深度都不能超过max_stack项所暗示的深度。
· 在执行过程中,从操作数堆栈中弹出的值不能多于它所包含的值。
· 在执行过程中,持有long或double类型数值的局部变量对的顺序不能被颠倒,也不能将这对变量拆开。在任何时候都不能对这样一对局部变量进行单独操作。
· 任何局部变量(或局部变量对,if是long或double类型的值)在被赋值之前都不能被访问。
· 每条invokespecial指令必须命名一个实例初始化方法(§2.9)、当前类或接口中的一个方法、当前类的超类中的一个方法、当前类或接口的直接超接口中的一个方法,或者Object的一个方法。
当一个实例初始化方法被调用时,一个未初始化的类实例必须在操作数栈中的适当位置。实例初始化方法决不能在一个初始化的类实例上调用。
if一条invokespecial指令命名了一个实例初始化方法,并且操作栈上的目标引用是当前类的一个未初始化的类实例,那么invokespecial必须命名当前类或其直接超类中的一个实例初始化方法。
if一条invokespecial指令命名了一个实例初始化方法,并且操作数堆栈上的目标引用是一个由早期new指令创建的类实例,那么invokespecial必须命名一个来自该类实例的实例初始化方法。
if一条invokespecial指令命名了一个不是实例初始化方法的方法,那么操作数栈上的目标引用的类型必须与当前类的赋值兼容(JLS §5.2)。
· 每个实例初始化方法,除了从Object类的构造函数派生的实例初始化方法外,在访问其实例成员之前,必须调用本类的另一个实例初始化方法或其直接超类super的实例初始化方法。
然而,在调用任何实例初始化方法之前,在当前类中声明的this的实例字段可以被分配。
· 当任何实例方法被调用或任何实例变量被访问时,包含该实例方法或实例变量的类实例必须已经被初始化。
· if在受异常处理程序保护的代码中的局部变量中有一个未初始化的类实例,那么 i) if处理程序在<init>方法内,处理程序必须抛出一个异常或永远循环; ii) if处理程序不在<init>方法内,未初始化的类实例必须保持未初始化。
· 在执行jsr或jsr_w指令时,操作栈上或局部变量中决不能有未初始化的类实例。
· 每个作为方法调用指令目标的类实例的类型必须与指令中指定的类或接口类型赋值兼容(JLS §5.2)。
· 每个方法调用的参数类型必须与方法描述符的方法调用兼容(JLS §5.3, §4.3.3)。
· 每个返回指令都必须与它的方法的返回类型相匹配。
o if方法返回一个布尔值、字节、char、short或int,只能使用ireturn指令。
o if方法返回的是浮点数、长数或双数,只能分别使用freturn、lreturn或dreturn指令。
o if方法返回一个引用类型,只能使用areturn指令,并且返回值的类型必须与方法的返回描述符赋值兼容(JLS §5.2, §4.3.3)。
o 所有的实例初始化方法、类或接口初始化方法以及声明返回void的方法必须只使用返回指令。
· 由getfield指令访问的或由putfield指令修改的每个类实例的类型必须与指令中指定的类类型赋值兼容(JLS §5.2)。
· 由putfield或putstatic指令存储的每个值的类型必须与被存储到的类实例或类的字段描述符(§4.3.2)兼容。
o if描述符类型是布尔型、字节型、char型、short型或int型,那么值必须是int型。
o if描述符类型是float、long或double,那么该值必须分别是float、long或double。
o if描述符类型是一个引用类型,那么值必须是一个与描述符类型赋值兼容的类型(JLS §5.2)。
· 通过aastore指令存储到数组中的每个值的类型必须是一个引用类型。
aastore指令所存储的数组的组件类型也必须是参考类型。
· 每条athrow指令必须只抛出Throwable类或Throwable子类的实例的值。
在方法的 Code_attribute结构的 exception_table数组的 catch_type项中提到的每一个类都必须是 Throwable或 Throwable的子类。
· ifgetfield或putfield被用来访问一个在超类中声明的受保护字段,而该超类是与当前类不同的运行时包的成员,那么被访问的类实例的类型必须与当前类相同或为其子类。
if invokevirtual 或 invokespecial 被用来访问一个在超类中声明的受保护方法,而该超类是与当前类不同的运行时包的成员,那么被访问的类实例的类型必须与当前类相同或为其子类。
· 执行永远不会从代码阵列的底部掉下来。
· 不得从局部变量中加载返回地址(returnAddress类型的值)。
· 每条jsr或jsr_w指令之后的指令只能通过一条ret指令返回。
· if一个子程序已经存在于子程序调用链中,则返回的jsr或jsr_w指令不得用于递归调用该子程序。(当使用try-finally结构时,子程序可以嵌套在final子句中)。
· 每个returnAddress类型的实例最多可以被返回一次。
if一条ret指令返回到子程序调用链中与ret类型的给定实例相对应的ret指令之上的一个点,那么该实例永远不能被用作返回地址。
4.10.验证类文件
尽管Java编程语言的编译器必须只产生满足前几节中所有静态和结构约束的类文件,但Java虚拟机不能保证它被要求加载的任何文件是由该编译器生成的,或者是正确形成的。像网络浏览器这样的应用程序不会下载源代码,然后再进行编译;这些应用程序会下载已经编译好的类文件。浏览器需要确定该类文件是由一个值得信赖的编译器产生的,还是由一个试图利用Java虚拟机的对手产生的。
编译时检查的另一个问题是版本偏移。一个用户可能已经成功地编译了一个类,比如PurchaseStockOptions,成为TradingClass的一个子类。但是TradingClass的定义可能已经在编译该类时发生了变化,其方式与之前存在的二进制文件不兼容。方法可能已经被删除,或者改变了它们的返回类型或修改器。字段可能改变了类型或从实例变量变成了类变量。方法或变量的访问修饰符可能已经从公共的变成了私有的。关于这些问题的讨论,请参阅\《Java语言规范》(Java SE 8版)中的第13章 “二进制兼容性”。
由于这些潜在的问题,Java虚拟机需要自己验证它试图加入的类文件是否满足所需的约束。Java虚拟机的实现会在链接时验证每个类文件是否满足必要的约束条件(第5.4节)。
链接时验证增强了运行时解释器的性能。否则,在运行时为每条解释指令验证约束条件而必须进行的昂贵的检查可以被消除。Java虚拟机可以假定这些检查已经被执行了。例如,Java虚拟机已经知道以下情况。
· 没有操作数堆栈溢出或欠溢出。
· 所有本地变量的使用和存储都是有效的。
· 所有Java虚拟机指令的参数都是有效类型。
有两种策略,Java虚拟机的实现可用于验证。
· 通过类型检查验证必须用于验证版本号大于或等于50.0的类文件。
· 所有的Java虚拟机实现都必须支持类型推理验证,符合Java ME CLDC和Java Card配置文件的除外,以便验证版本号小于50.0的类文件。
对支持Java ME CLDC和Java Card配置文件的Java虚拟机实现的验证由其各自的规范管理。
在这两种策略中,验证主要是在代码属性的代码阵列上执行第4.9节的静态和结构约束(第4.7.3节)。然而,在验证过程中,在代码属性之外还有三个额外的检查必须被执行。
· 确保最终类不被子类化。
· 确保最终方法不被重写(§5.4.5)。
· 检查每个类(除了Object)都有一个直接的超类。
4.10.1.通过类型检查进行验证
一个版本号为50.0或以上(§4.1)的类文件必须使用本节给出的类型检查规则进行验证。
if,也只有在类文件的版本号等于 50.0 的情况下,if类型检查失败,Java 虚拟机实现可以选择尝试通过类型推理进行验证(§4.10.2)。
这是一个务实的调整,旨在缓解向新的验证纪律的过渡。许多操作类文件的工具可能会以需要调整方法的堆栈映射框架的方式改变方法的字节码。if一个工具没有对堆栈映射框架进行必要的调整,类型检查可能会失败,即使字节码在原则上是有效的(并且在旧的类型推理方案下会随之验证)。为了让实现者有时间来调整他们的工具,Java虚拟机的实现可以回到旧的验证规则,但只能在有限的时间内。
在类型检查失败但类型推理被调用并成功的情况下,预计会有一定的性能损失。这种惩罚是不可避免的。它也应该作为一个信号给工具供应商,表明他们的输出需要调整,并为供应商提供额外的激励来进行这些调整。
总之,通过类型推理进行故障转移验证,既支持在Java SE平台上逐步增加堆栈映射框架(if它们不存在于50.0版本的类文件中,则允许故障转移),也支持在Java SE平台上逐步删除jsr和jsr_w指令(if它们存在于50.0版本的类文件中,则允许故障转移)。
if一个 Java 虚拟机实现试图在 50.0 版本的类文件上通过类型推理进行验证,它必须在通过类型检查验证失败的所有情况下这样做。
这意味着Java虚拟机的实现不能选择在一种情况下采用类型推理,而在另一种情况下不采用。它必须拒绝那些没有通过类型检查进行验证的类文件,否则在类型检查失败时,必须持续地故障转移到类型推理验证器。
类型检查器执行的类型规则是通过Prolog条款来指定的。英语文本被用来以非正式的方式描述类型规则,而Prolog条款提供了正式的规范。
类型检查器需要为每个带有Code属性的方法提供一个堆栈图框列表(§4.7.3)。堆栈映射框架的列表由Code属性的StackMapTable属性(§4.7.4)给出。其目的是,堆栈映射框架必须出现在方法中每个基本块的开头。堆栈映射框架指定了每个基本块开始时的每个操作数堆栈条目和每个局部变量的验证类型。类型检查器为每个具有代码属性的方法读取堆栈图框,并使用这些图来生成代码属性中指令的类型安全证明。
if一个类的所有方法都是类型安全的,并且它没有子类化一个最终类,那么这个类就是类型安全的。
classIsTypeSafe(Class) :-
classClassName(Class, Name)。
classDefiningLoader(Class, L)。
superclassChain(Name, L, Chain)。
Chain (链)= [],
classSuperClassName(Class, SuperclassName)。
loadedClass(SuperclassName, L, Superclass)。
classIsNotFinal(Superclass)。
classMethods(Class, Methods)。
checklist(methodIsTypeSafe(Class), Methods)。
classIsTypeSafe(Class) :-
classClassName(Class, 'java/lang/Object')。
classDefiningLoader(Class, L)。
isBootstrapLoader(L)。
classMethods(Class, Methods)。
checklist(methodIsTypeSafe(Class), Methods)。
Prolog谓词classIsTypeSafe假定Class是一个Prolog术语,代表一个已经被成功解析和加载的二进制类。本规范没有规定这个术语的精确结构,但要求在它上面定义某些谓词。
例如,我们假设有一个谓词classMethods(Class, Methods),它给定一个代表上述类的术语作为它的第一个参数,将它的第二个参数绑定到一个包含该类所有方法的列表上,该列表以一种方便的形式表示,稍后描述。
if谓词classIsTypeSafe不为真,类型检查器必须抛出异常VerifyError,以表明类文件是畸形的。否则,类文件已经成功地进行了类型检查,字节码验证已经成功完成。
本节的其余部分将详细解释类型检查的过程。
· 首先,我们为核心的Java虚拟机工件如类和方法给出Prolog谓词(§4.10.1.1)。
· 第二,我们指定类型检查器所知道的类型系统(§4.10.1.2)。
· 第三,我们指定指令和堆栈图帧的Prolog表示(§4.10.1.3,§4.10.1.4)。
· 第四,我们指定一个方法是如何被类型检查的,对于没有代码的方法(§4.10.1.5)和有代码的方法(§4.10.1.6)。
· 第五,我们讨论了所有加载和存储指令共有的类型检查问题(§4.10.1.7),以及对受保护成员的访问问题(§4.10.1.8)。
· 最后,我们指定规则对每条指令进行类型检查(§4.10.1.9)。
4.10.1.1.Java虚拟机构件的访问器
我们规定存在28个Prolog谓词(“accessors”),这些谓词具有某些预期行为,但其正式定义没有在本规范中给出。
classClassName(Class, ClassName)。
提取类Class的名称ClassName。
classIsInterface(Class)
if类,即Class是一个接口,则为真。
classIsNotFinal(Class)
if这个类,即Class,不是一个最终类,则为真。
classSuperClassName(Class, SuperClassName)
提取Class的超类的名称,SuperClassName。
classInterfaces(Class, Interfaces)
提取类的直接超接口的列表,即Interfaces。
classMethods(Class, Methods)
提取Class中声明的方法的列表Methods。
classAttributes(Class, Attributes)
提取Class的属性列表,即Attributes。
每个属性都被表示为形式为 attribute(AttributeName, AttributeContents)的函数应用,其中 AttributeName是属性的名称。属性内容的格式是未指定的。
classDefiningLoader(Class, Loader)
提取Class的定义类加载器,Loader。
isBootstrapLoader(Loader)
if类加载器Loader是引导类加载器,则为真。
loadedClass(Name, InitiatingLoader, ClassDefinition)
if存在一个名为Name的类,当被类加载器InitiatingLoader加载时,其表示方法(根据本规范)是ClassDefinition,则为真。
methodName(Method, Name)
提取方法Method的名称,Name,。
methodAccessFlags(Method, AccessFlags)
提取方法Method的访问标志,AccessFlags。
methodDescriptor(Method, Descriptor)
提取方法Method的描述符,Descriptor。
methodAttributes(Method, Attributes)
提取方法Method的属性列表,Attributes。
isInit(方法)
if方法(不管是什么类别)是<init>,则为真。
isNotInit(方法)
if方法(无论其类别)不是<init>,则为真。
isNotFinal(Method, Class)
if类中的方法不是最终的,则为真。
isStatic(Method, Class)
if类中的方法是静态的,则为真。
isNotStatic(Method, Class)
if类中的方法不是静态的,则为真。
isPrivate(Method, Class)
if类中的方法是私有的,则为真。
isNotPrivate(Method, Class)
if类中的方法不是私有的,则为真。
isProtected(MemberClass, MemberName, MemberDescriptor)
if在类MemberClass中存在一个名为MemberName的成员,其描述符为MemberDescriptor,并且是受保护的,则为真。
isNotProtected(MemberClass, MemberName, MemberDescriptor)
if在类MemberClass中存在一个名为MemberName的成员,其描述符为MemberDescriptor,并且不受保护,则为真。
parseFieldDescriptor(Descriptor, Type)
将一个字段描述符Descriptor转换为相应的验证类型Type(§4.10.1.2)。
parseMethodDescriptor(Descriptor, ArgTypeList, ReturnType)。
将方法描述符Descriptor转换为对应于方法参数类型的验证类型ArgTypeList和对应于返回类型的验证类型ReturnType的列表。
parseCodeAttribute(Class, Method, FrameSize, MaxStack, ParsedCode, Handlers, StackMap)
提取类中方法Method的指令流ParsedCode,以及最大操作数栈MaxStack、最大局部变量数FrameSize、异常处理程序Handlers和栈图StackMap。
指令流和堆栈图属性的表示必须符合§4.10.1.3和§4.10.1.4的规定。
相同的包名(Class1, Class2)
ifClass1和Class2的包名相同,则为真。
differentPackageName(Class1, Class2)
ifClass1和Class2的包名不同,则为真。
当对一个方法的主体进行类型检查时,访问关于该方法的信息是很方便的。为了这个目的,我们定义了一个环境,一个由以下内容组成的六元组。
· 阶层
· 方法
· 宣布的方法的返回类型
· 方法中的指令
· 操作数栈的最大尺寸
· 异常处理程序的列表
我们指定访问器来从环境中提取信息。
allInstructions(Environment, Instructions) :-
环境 = environment(_Class, _Method, _ReturnType,
指示,_,_)。
exceptionHandlers(Environment, Handlers) :-
环境 = environment(_Class, _Method, _ReturnType,
_指令,_,处理者)。
maxOperandStackLength(Environment, MaxStack) :-
环境 = environment(_Class, _Method, _ReturnType,
_Instructions, MaxStack, _Handlers)。
thisClass(Environment, class(ClassName, L)) :-
环境 = environment(Class, _Method, _ReturnType,
_指示,_,_)。
classDefiningLoader(Class, L)。
classClassName(Class, 类名)。
thisMethodReturnType(Environment, ReturnType) :-
环境 = environment(_Class, _Method, ReturnType,
_指示,_,_)。
我们指定额外的谓词来从环境中提取更高层次的信息。
offsetStackFrame(Environment, Offset, StackFrame) :-
allInstructions(Environment, Instructions)。
member(stackMap(Offset, StackFrame), Instructions)。
currentClassLoader(Environment, Loader) :-
thisClass(Environment, class(_, Loader))。
最后,我们指定一个贯穿类型规则的一般谓词。
notMember(_, [])。
notMember(X, [A | More]) :- X\= A, notMember(X, More)。
指导决定哪些访问器是规定的,哪些是完全指定的原则是,我们不想过度指定类文件的表示。为类或方法术语提供特定的访问器将迫使我们完全指定代表类文件的Prolog术语的格式。
4.10.1.2.核查类型系统
类型检查器根据验证类型的层次结构来执行一个类型系统,如下图所示。
核查类型的层次结构。
顶部
____________/\____________
/ \
/ \
一个字 两个字
/ | \ / \
/ | \ / \
int float reference long double
/ \
/ \_____________
/ \
/ \
未初始化的 +------------------+
/ ǞǞǞǞ
/ /类型 层次结构| /
uninitializedThis uninitialized(Offset) +------------------+
|
|
无
大多数验证类型与表4.3-A中字段描述符所代表的基元和参考类型有直接的对应关系。
· 原始类型double、float、int和long(字段描述符D、F、I、J)分别对应于同名的验证类型。
· 原始类型byte、char、short和boolean(字段描述符B、C、S、Z)都对应于验证类型int。
· 类和接口类型对应于使用漏斗类的验证类型。注意,L是class(N,L)所代表的类的启动加载器(§5.3),可能是,也可能不是,该类的定义加载器。
例如,类的类型Object将被表示为class('java/lang/Object', BL),其中BL是bootstrap loader。
· 数组类型对应于使用函数器arrayOf的验证类型。验证类型arrayOf(T)表示其组成类型为验证类型T的数组类型。
例如,int[]和Object[]类型将分别由arrayOf(int)和arrayOf(class('java/lang/Object', BL))表示。
验证类型uninitialized(Offset)是通过对代表Offset数值的参数应用uninitialized的函数器来表示。
其他验证类型在Prolog中被表示为原子,其名称表示有关的验证类型。
验证类型的子类型化规则如下。
子类型化是反身性的。
isAssignable(X, X)。
在Java编程语言中不属于引用类型的验证类型有如下形式的子类型规则。
isAssignable(v, X) :- isAssignable(the_direct_supertype_of_v, X)。
也就是说,ifv的直接超类型是X的一个子类型,那么v就是X的一个子类型。
isAssignable(oneWord, top)。
isAssignable(twoWord, top)。
isAssignable(int, X) :- isAssignable(oneWord, X)。
isAssignable(float, X) :- isAssignable(oneWord, X)。
isAssignable(long, X) :- isAssignable(twoWord, X)。
isAssignable(double, X) :- isAssignable(twoWord, X)。
isAssignable(reference, X) :- isAssignable(oneWord, X)。
isAssignable(class(_, _), X) :- isAssignable(reference, X)。
isAssignable(arrayOf(_), X) :- isAssignable(reference, X)。
isAssignable(uninitialized, X) :- isAssignable(reference, X)。
isAssignable(uninitializedThis, X) :- isAssignable(uninitialized, X)。
isAssignable(uninitialized(_), X) :- isAssignable(uninitialized, X)。
isAssignable(null, class(_, _))。
isAssignable(null, arrayOf(_))。
isAssignable(null, X) :- isAssignable(class('java/lang/Object', BL), X)。
isBootstrapLoader(BL)。
这些子类型规则不一定是子类型化的最明显的表述。在Java编程语言中的引用类型的子类型规则和其余验证类型的规则之间有一个明显的分割。这种分割使我们能够说明Java编程语言的引用类型和其他验证类型之间的一般子类型关系。这些关系独立于Java引用类型在类型层次中的位置,并有助于防止Java虚拟机实现的过度类加载。例如,我们不希望在回应class(foo, L) <: twoWord.L>这样的查询时开始攀登Java超类的层次。
我们还有一条规则,说子类型是反身的,所以这些规则加起来涵盖了Java编程语言中不属于引用类型的大多数验证类型。
Java编程语言中引用类型的子类型规则是用isJavaAssignable递归指定的。
isAssignable(class(X, Lx), class(Y, Ly)) :-
isJavaAssignable(class(X, Lx), class(Y, Ly))。
isAssignable(arrayOf(X), class(Y, L)) :-
isJavaAssignable(arrayOf(X), class(Y, L))。
isAssignable(arrayOf(X), arrayOf(Y)) :-
isJavaAssignable(arrayOf(X), arrayOf(Y))。
对于赋值来说,接口被当作Object来处理。
isJavaAssignable(class(_, _), class(To, L) :-
loadedClass(To, L, ToClass)。
classIsInterface(ToClass)。
isJavaAssignable(From, To) :-
isJavaSubclassOf(From, To)。
阵列类型是Object的子类型。其目的也是为了让数组类型成为Cloneable和java.io.Serializable的子类型。
isJavaAssignable(arrayOf(_), class('java/lang/Object', BL)) :-
isBootstrapLoader(BL)。
isJavaAssignable(arrayOf(_), X) :-
isArrayInterface(X)。
isArrayInterface(class('java/lang/Cloneable', BL)) :-
isBootstrapLoader(BL)。
isArrayInterface(class('java/io/Serializable', BL)) :-
isBootstrapLoader(BL)。
原始类型的数组之间的子类型化是身份关系。
isJavaAssignable(arrayOf(X), arrayOf(Y)) :-
atom(X)。
atom(Y),
X=Y。
引用类型的数组之间的子类型是共变的。
isJavaAssignable(arrayOf(X), arrayOf(Y)) :-
compound(X), compound(Y), isJavaAssignable(X, Y)。
子类是反身性的。
isJavaSubclassOf(class(SubclassName, L), class(SubclassName, L))。
isJavaSubclassOf(class(SubclassName, LSub), class(SuperclassName, LSuper)) :-
superclassChain(SubclassName, LSub, Chain)。
member(class(SuperclassName, L), Chain)。
loadedClass(SuperclassName, L, Sup)。
loadedClass(SuperclassName, LSuper, Sup)。
superclassChain(ClassName, L, [class(SuperclassName, Ls) | Rest]) :-
loadedClass(ClassName, L, Class)。
classSuperClassName(Class, SuperclassName)。
classDefiningLoader(Class, Ls)。
superclassChain(SuperclassName, Ls, Rest)。
superclassChain('java/lang/Object', L, []) :-
loadedClass('java/lang/Object', L, Class),
classDefiningLoader(Class, BL)。
isBootstrapLoader(BL)。
4.10.1.3.指令表示法
单个字节码指令在Prolog中被表示为术语,其漏斗是指令的名称,其参数是其解析的操作数。
例如,一条aload指令被表示为术语aload(N),其中包括作为指令操作数的索引N。
指示作为一个整体被表示为一个形式的术语列表。
指令(Offset, AnInstruction)。
例如,指令(21, aload(1))。
这个列表中的指令顺序必须与类文件中的相同。
一些指令的操作数是代表字段、方法和动态调用站点的常量池条目。在常量池中,字段由CONSTANT_Fieldref_info结构表示,方法由CONSTANT_InterfaceMethodref_info结构(对于接口的方法)或CONSTANT_Methodref_info结构(对于类的方法)表示,而动态调用点由CONSTANT_InvokeDynamic_info结构表示(§4.4.2, §4.4.10)。这样的结构被表示为形式为的漏斗应用。
· field(FieldClassName, FieldName, FieldDescriptor)为一个字段,其中FieldClassName是CONSTANT_Fieldref_info结构中class_index项所引用的类的名称,FieldName和FieldDescriptor对应于CONSTANT_Fieldref_info结构中name_and_type_index项所引用的名称和字段描述器。
· imethod(MethodIntfName, MethodName, MethodDescriptor)为一个接口的方法,其中MethodIntfName是CONSTANT_InterfaceMethodref_info结构的class_index项所引用的接口名称,MethodName和MethodDescriptor对应于CONSTANT_InterfaceMethodref_info结构的name_and_type_index项所引用的名称和方法描述器。
· method(MethodClassName, MethodName, MethodDescriptor)为一个类的方法,其中MethodClassName是CONSTANT_Methodref_info结构中class_index项所引用的类的名称,MethodName和MethodDescriptor对应于CONSTANT_Methodref_info结构中name_and_type_index项所引用的名称和方法描述符;以及
· dmethod(CallSiteName, MethodDescriptor)用于动态调用站点,其中CallSiteName和MethodDescriptor对应于CONSTANT_InvokeDynamic_info结构中name_and_type_index项所引用的名称和方法描述符。
为了清楚起见,我们假设字段和方法描述符(§4.3.2, §4.3.3)被映射成更易读的名字:类名中的前导L和后导;被删除,用于原始类型的BaseType字符被映射到这些类型的名字中。
例如,一条getfield指令,其操作数是常量池中的索引,指的是Bar类中F类型的字段foo,将被表示为getfield(field('Bar', 'foo', 'F'))。
引用常量值的常量池条目,如CONSTANT_String、CONSTANT_Integer、CONSTANT_Float、CONSTANT_Long、CONSTANT_Double和CONSTANT_Class,是通过名字分别为string、int、float、long、double和classConstant的functors来编码的。
例如,一条加载整数91的ldc指令将被编码为ldc(int(91))。
4.10.1.4.堆栈图框架表示法
堆栈图框架在Prolog中被表示为一个形式的术语列表。
stackMap(Offset, TypeState)
其中。
· Offset是一个整数,表示堆栈映射框架适用的字节码偏移(§4.7.4)。
这个列表中的字节码偏移量的顺序必须与类文件中的相同。
· TypeState是Offset处指令的预期传入类型状态。
类型状态是一个从操作数栈中的位置和方法的局部变量到验证类型的映射。它的形式是
frame(Locals, OperandStack, Flags)
其中。
· Locals是一个验证类型的列表,这样列表中的第i个元素(基于0的索引)代表局部变量i的类型。
· OperandStack是一个验证类型的列表,这样,列表的第一个元素代表操作数堆栈顶部的类型,顶部以下的堆栈条目的类型在列表中以适当的顺序跟随。
大小为2的类型(long和double)由两个堆栈条目表示,第一个条目是顶部,第二个条目是类型本身。
例如,一个有一个双倍值、一个int值和一个long值的堆栈在类型状态下被表示为一个有五个条目的堆栈:双倍值的top和double条目,int值的int条目,long值的top和long条目。相应地,OperandStack是列表[top, double, int, top, long]。
· 旗帜是一个列表,可以是空的,也可以有单一元素flagThisUninit。
ifLocals中的任何局部变量具有uninitializedThis类型,那么Flags就有单一元素flagThisUninit,否则Flags就是一个空列表。
flagThisUninit在构造函数中被用来标记类型状态,在这种状态下,这个方法的初始化还没有完成。在这种类型状态下,从该方法返回是非法的。
验证类型的子类型化被点状扩展到类型状态。
一个方法的局部变量数组在构造上有一个固定的长度(见§4.10.1.6中的methodInitialStackFrame),而操作数栈则会增长和缩小。因此,我们需要对操作数栈的长度进行明确的检查,这些操作数栈的可分配性是需要的。
frameIsAssignable(frame(Locals1, StackMap1, Flags1),
frame(Locals2, StackMap2, Flags2)) :-
length(StackMap1, StackMapLength)。
length(StackMap2, StackMapLength)。
maplist(isAssignable, Locals1, Locals2)。
maplist(isAssignable, StackMap1, StackMap2)。
subset(Flags1, Flags2)。
操作数堆栈的长度不得超过宣布的最大堆栈长度。
operandStackHasLegalLength(Environment, OperandStack) :-
length(OperandStack, Length)。
maxOperandStackLength(环境,MaxStack)。
长度=< MaxStack.
某些数组指令(§aaload, §arraylength, §baload, §bastore)偷看操作数栈上的值的类型,以检查它们是数组类型。下面的条款从类型状态中访问操作数栈的第i个元素。
nth1OperandStackIs(i, frame(_Locals, OperandStack, _Flags), Element) :-
nth1(i, OperandStack, Element)。
通过加载和存储指令(§4.10.1.7)对操作数堆栈的操作由于某些类型在堆栈上占用两个条目而变得复杂。下面给出的谓词考虑到了这一点,使规范的其余部分可以从这个问题中抽象出来。
从堆栈中弹出一个类型列表。
canPop(frame(Locals, OperandStack, Flags), Types,
frame(Locals, PoppedOperandStack, Flags)) :-
popMatchingList(OperandStack, Types, PoppedOperandStack)。
popMatchingList(OperandStack, [], OperandStack)。
popMatchingList(OperandStack, [P | Rest], NewOperandStack) :-
popMatchingType(OperandStack, P, TempOperandStack, _ActualType)。
popMatchingList(TempOperandStack, Rest, NewOperandStack)。
从堆栈中弹出一个单独的类型。更准确地说,if堆栈的逻辑顶部是指定类型Type的某个子类型,那么就弹出它。if一个类型占据了两个堆栈条目,那么堆栈的逻辑顶部实际上是就在顶部下面的类型,而堆栈的顶部是不可用的类型顶部。
popMatchingType([ActualType | OperandStack],
Type, OperandStack, ActualType) :-
sizeOf(Type, 1)。
isAssignable(ActualType, Type)。
popMatchingType([top, ActualType | OperandStack],
Type, OperandStack, ActualType) :-
sizeOf(Type, 2)。
isAssignable(ActualType, Type)。
sizeOf(X, 2) :- isAssignable(X, twoWord)。
sizeOf(X, 1) :- isAssignable(X, oneWord)。
sizeOf(top, 1)。
将一个逻辑类型推入堆栈。具体行为随类型的大小而变化。if被推的类型是大小为1的,我们只是把它推到栈上。if被推的类型是大小为2的,我们就推它,然后再推顶。
pushOperandStack(OperandStack, 'void', OperandStack)。
pushOperandStack(OperandStack, Type, [Type | OperandStack]) :-
sizeOf(Type, 1)。
pushOperandStack(OperandStack, Type, [top, Type | OperandStack] ) :-
sizeOf(Type, 2)。
if有空间的话,把一个类型的列表推到栈上。
canSafelyPush(Environment, InputOperandStack, Type, OutputOperandStack) :-
pushOperandStack(InputOperandStack, Type, OutputOperandStack)。
operandStackHasLegalLength(Environment, OutputOperandStack)。
canSafelyPushList(Environment, InputOperandStack, Types,
OutputOperandStack) :-
canPushList(InputOperandStack, Types, OutputOperandStack)。
operandStackHasLegalLength(Environment, OutputOperandStack)。
canPushList(InputOperandStack, [], InputOperandStack)。
canPushList(InputOperandStack, [Type | Rest], OutputOperandStack) :-
pushOperandStack(InputOperandStack, Type, InterimOperandStack)。
canPushList(InterimOperandStack, Rest, OutputOperandStack)。
dup指令对操作数堆栈的操作完全是根据堆栈上的值的类型类别来规定的(§2.11.1)。
第1类类型占据了一个堆栈条目。if堆栈的顶部是Type,并且Type不是顶部(否则它可能表示第2类类型的上半部分),那么将第1类逻辑类型Type从堆栈中弹出是可能的。其结果是传入的堆栈,顶部的条目被弹出。
popCategory1([Type | Rest], Type, Rest) :-
Type /= top,
sizeOf(Type, 1)。
第2类类型占据了两个堆栈条目。if堆栈的顶部是type top,而其正下方的条目是Type,那么从堆栈中弹出一个第2类的逻辑类型Type是可能的。其结果是传入的堆栈,上面的两个条目被弹出。
popCategory2([top, Type | Rest], Type, Rest) :-
sizeOf(Type, 2)。
大多数单个指令的类型规则(§4.10.1.9)取决于有效的类型转换概念。if可以从传入的类型状态的操作数堆栈中弹出一个预期的类型列表,并用预期的结果类型替换它们,从而形成一个新的有效的类型状态,那么这个类型转换就是有效的。特别是,新类型状态下的操作数栈的大小不能超过其最大的声明大小。
validTypeTransition(Environment, ExpectedTypesOnStack, ResultType,
frame(Locals, InputOperandStack, Flags)。
frame(Locals, NextOperandStack, Flags)) :-
popMatchingList(InputOperandStack, ExpectedTypesOnStack,
InterimOperandStack)。
pushOperandStack(InterimOperandStack, ResultType, NextOperandStack)。
operandStackHasLegalLength(Environment, NextOperandStack)。
4.10.1.5.类型检查抽象和本地方法
if抽象方法和本地方法没有覆盖最终方法,那么它们被认为是类型安全的。
methodIsTypeSafe(Class, Method) :-
doesNotOverrideFinalMethod(Class, Method)。
methodAccessFlags(Method, AccessFlags)。
member(abstract, AccessFlags)。
methodIsTypeSafe(Class, Method) :-
doesNotOverrideFinalMethod(Class, Method)。
methodAccessFlags(Method, AccessFlags)。
member(native, AccessFlags)。
私有方法和静态方法与动态方法调度是正交的,所以它们从不覆盖其他方法(§5.4.5)。
doesNotOverrideFinalMethod(class('java/lang/Object', L), Method) :-
isBootstrapLoader(L)。
doesNotOverrideFinalMethod(Class, Method) :-
isPrivate(Method, Class)。
doesNotOverrideFinalMethod(Class, Method) :-
isStatic(Method, Class)。
doesNotOverrideFinalMethod(Class, Method) :-
isNotPrivate(Method, Class)。
isNotStatic(Method, Class)。
doesNotOverrideFinalMethodOfSuperclass(Class, Method)。
doesNotOverrideFinalMethodOfSuperclass(Class, Method) :-
classSuperClassName(Class, SuperclassName)。
classDefiningLoader(Class, L)。
loadedClass(SuperclassName, L, Superclass)。
classMethods(Superclass, SuperMethodList)。
finalMethodNotOverridden(Method, Superclass, SuperMethodList)。
私有和/或静态的最终方法是不寻常的,因为私有方法和静态方法本身是不能被重写的。因此,if发现一个最终的私有方法或最终的静态方法,从逻辑上讲,它没有被其他方法重写。
finalMethodNotOverridden(Method, Superclass, SuperMethodList) :-
methodName(Method, Name)。
methodDescriptor(Method, Descriptor)。
member(method(_, Name, Descriptor), SuperMethodList)。
isFinal(Method, Superclass)。
isPrivate(Method, Superclass)。
finalMethodNotOverridden(Method, Superclass, SuperMethodList) :-
methodName(Method, Name)。
methodDescriptor(Method, Descriptor)。
member(method(_, Name, Descriptor), SuperMethodList)。
isFinal(Method, Superclass)。
isStatic(Method, Superclass)。
if发现了一个非最终的私有方法或非最终的静态方法,请跳过它,因为它与重写是正交的。
finalMethodNotOverridden(Method, Superclass, SuperMethodList) :-
methodName(Method, Name)。
methodDescriptor(Method, Descriptor)。
member(method(_, Name, Descriptor), SuperMethodList)。
isNotFinal(Method, Superclass)。
isPrivate(Method, Superclass)。
doesNotOverrideFinalMethodOfSuperclass(Superclass, Method)。
finalMethodNotOverridden(Method, Superclass, SuperMethodList) :-
methodName(Method, Name)。
methodDescriptor(Method, Descriptor)。
member(method(_, Name, Descriptor), SuperMethodList)。
isNotFinal(Method, Superclass)。
isStatic(Method, Superclass)。
doesNotOverrideFinalMethodOfSuperclass(Superclass, Method)。
if找到了一个非最终的、非私有的、非静态的方法,那么确实是一个最终方法没有被重载。否则,向上递归。
finalMethodNotOverridden(Method, Superclass, SuperMethodList) :-
methodName(Method, Name)。
methodDescriptor(Method, Descriptor)。
member(method(_, Name, Descriptor), SuperMethodList)。
isNotFinal(Method, Superclass)。
isNotStatic(Method, Superclass)。
isNotPrivate(Method, Superclass)。
finalMethodNotOverridden(Method, Superclass, SuperMethodList) :-
methodName(Method, Name)。
methodDescriptor(Method, Descriptor)。
notMember(method(_, Name, Descriptor), SuperMethodList)。
doesNotOverrideFinalMethodOfSuperclass(Superclass, Method)。
4.10.1.6.用代码进行类型检查的方法
if非抽象的、非本地的方法有代码,并且代码是类型正确的,那么它们就是类型正确的。
methodIsTypeSafe(Class, Method) :-
doesNotOverrideFinalMethod(Class, Method)。
methodAccessFlags(Method, AccessFlags)。
methodAttributes(Method, Attributes)。
notMember(native, AccessFlags)。
notMember(abstract, AccessFlags)。
member(attributee('Code', _), Attributes),
methodWithCodeIsTypeSafe(Class, Method)。
if有可能将代码和堆栈地图帧合并成一个流,使每个堆栈地图帧在它所对应的指令之前,并且合并后的流是正确的,那么带有代码的方法就是类型安全的。该方法的异常处理程序,if有的话,也必须是合法的。
methodWithCodeIsTypeSafe(Class, Method) :-
parseCodeAttribute(Class, Method, FrameSize, MaxStack,
ParsedCode, Handlers, StackMap)。
mergeStackMapAndCode(StackMap, ParsedCode, MergedCode)。
methodInitialStackFrame(Class, Method, FrameSize, StackFrame, ReturnType)。
环境 = environment(Class, Method, ReturnType, MergedCode,
MaxStack, Handlers)。
handlersAreLegal(Environment)。
mergedCodeIsTypeSafe(Environment, MergedCode, StackFrame)。
让我们先考虑异常处理程序。
异常处理程序是由一个形式为Functor的应用来表示的。
handler(Start, End, Target, ClassName)。
其参数分别是处理程序覆盖的指令范围的开始和结束,处理程序代码的第一条指令,以及这个处理程序被设计为处理的异常类的名称。
if一个异常处理程序的起点(Start)小于终点(End),存在一条偏移量等于Start的指令,存在一条偏移量等于End的指令,并且处理程序的异常类可以分配给Throwable类,那么该处理程序就是合法的。if处理程序的类条目是0,处理程序的异常类就是Throwable,否则就是处理程序中指定的类。
if处理程序覆盖的指令之一是<init>方法的 invokespecial,那么对<init>方法内的处理程序还有一个额外的要求。在这种情况下,处理程序正在运行的事实意味着正在构建的对象很可能被破坏,所以处理程序不能吞噬异常,并允许包围的 <init>方法正常返回给调用者,这一点很重要。因此,处理程序被要求要么通过向封闭的<init>方法的调用者抛出一个异常而突然完成,要么永远循环。
HandlersAreLegal(Environment) :-
exceptionHandlers(Environment, Handlers)。
checklist(handlerIsLegal(Environment), Handlers)。
handlerIsLegal(Environment, Handler) :-
Handler = handler(Start, End, Target, _)。
开始<结束。
allInstructions(Environment, Instructions)。
成员(指令(开始,_),指令)。
offsetStackFrame(Environment, Target, _)。
instructionsIncludeEnd(指令,结束)。
currentClassLoader(Environment, CurrentLoader)。
handlerExceptionClass(Handler, ExceptionClass, CurrentLoader)。
isBootstrapLoader(BL)。
isAssignable(ExceptionClass, class('java/lang/Throwable', BL))。
initHandlerIsLegal(环境,处理程序)。
instructionsIncludeEnd(指令, 结束) :-
成员(指令(End, _),指令)。
instructionsIncludeEnd(指令, 结束) :-
成员(endOfCode(End), Instructions)。
handlerExceptionClass(handler(_, _, _, 0),
class('java/lang/Throwable', BL), _) :-
isBootstrapLoader(BL)。
handlerExceptionClass(handler(_, _, _, Name),
class(Name, L), L) :-
Name\=0。
initHandlerIsLegal(Environment, Handler) :-
notInitHandler(环境,处理程序)。
notInitHandler(Environment, Handler) :-
环境 = environment(_Class, Method, _, Instructions, _, _)。
isNotInit(Method)。
notInitHandler(Environment, Handler) :-
环境 = environment(_Class, Method, _, Instructions, _, _)。
isInit(Method)。
成员(指令(_, invokespecial(CP)), 指令)。
CP = method(MethodClassName, MethodName, Descriptor)。
MethodName \= '<init>' 。
initHandlerIsLegal(Environment, Handler) :-
isInitHandler(环境,处理程序)。
sublist(isApplicableInstruction(Target), Instructions,
HandlerInstructions)。
noAttemptToReturnNormally(处理程序指令)。
isInitHandler(Environment, Handler) :-
环境 = environment(_Class, Method, _, Instructions, _, _)。
isInit(Method)。
成员(指令(_, invokespecial(CP)), 指令)。
CP = method(MethodClassName, '<init> ', Descriptor)。
isApplicableInstruction(HandlerStart, instruction(Offset, _)) :-
Offset >= HandlerStart.
noAttemptToReturnNormally(指示) :-
notMember(instruction(_, return), Instructions)。
noAttemptToReturnNormally(指示) :-
成员(指令(_, athrow), 指令)。
现在让我们来看看指令流和堆栈图框。
将指令和堆栈图帧合并成一个流涉及到四种情况。
· 合并一个空的StackMap和一个指令列表,可以得到原始的指令列表。
· mergeStackMapAndCode([], CodeList, CodeList)。
· 给出一个以Offset处指令的类型状态开始的堆栈图框列表,和一个以Offset开始的指令列表,合并后的列表是堆栈图框列表的头,然后是指令列表的头,最后是这两个列表尾部的合并。
· mergeStackMapAndCode([stackMap(Offset, Map) | RestMap].)
· [指令(Offset, Parse) | RestCode]。
· [stackMap(Offset, Map),
· 指令(Offset, Parse) | RestMerge]) :-
· mergeStackMapAndCode(RestMap, RestCode, RestMerge)。
· 否则,给定一个以偏移M处的指令类型状态开始的堆栈图框列表,和一个以偏移P开始的指令列表,那么,if偏移P<偏移M,合并后的列表由指令列表的头部组成,然后是堆栈图框列表和指令列表的尾部的合并。
· mergeStackMapAndCode([stackMap(OffsetM, Map) | RestMap].)
· [指令(OffsetP, Parse) | RestCode]。
· [指令(OffsetP, Parse) | RestMerge]) :-
· 偏移量P < 偏移量M。
· mergeStackMapAndCode([stackMap(OffsetM, Map) | RestMap].)
· RestCode,RestMerge)。
· 否则,这两个列表的合并是未定义的。由于指令列表的偏移量是单调递增的,除非每个堆栈地图帧的偏移量都有相应的指令偏移量,并且堆栈地图帧的顺序是单调递增的,否则两个列表的合并是不确定的。
为了确定一个方法的合并流的类型是否正确,我们首先推断出该方法的初始类型状态。
一个方法的初始类型状态包括一个空的操作栈和从this和参数的类型派生出来的局部变量类型,以及适当的标志,这取决于这是否是一个<init>方法。
methodInitialStackFrame(Class, Method, FrameSize, frame(Locals, [], Flags),
返回类型):-
methodDescriptor(Method, Descriptor)。
parseMethodDescriptor(Descriptor, RawArgs, ReturnType)。
expandTypeList(RawArgs, Args)。
methodInitialThisType(Class, Method, ThisList)。
flags(ThisList, Flags)。
append(ThisList, Args, ThisArgs)。
expandToLength(ThisArgs, FrameSize, top, Locals)。
给定一个类型的列表,下面的子句会产生一个列表,其中每个大小为2的类型都被两个条目所替代:一个是它自己,另一个是顶部条目。然后,该结果对应于列表在Java虚拟机中的32位字表示。
expandTypeList([], [])。
expandTypeList([项目 | 列表], [项目 | 结果]) :-
sizeOf(Item, 1)。
expandTypeList(List, Result)。
expandTypeList([Item | List], [Item, top | Result]) :-
sizeOf(Item, 2)。
expandTypeList(List, Result)。
flags([uninitializedThis], [flagThisUninit])。
flags(X, []) :- X\= [uninitializedThis] 。
expandToLength(List, Size, _Filler, List) :-
length(List, Size)。
expandToLength(List, Size, Filler, Result) :-
length(List, ListLength)。
ListLength < Size,
Delta是大小-ListLength。
length(Extra, Delta)。
Checklist(=(Filler), Extra)。
append(List, Extra, Result)。
对于一个实例方法的初始类型状态,我们计算this的类型并将其放入一个列表中。在Object的<init>方法中,this的类型是Object;在其他<init>方法中,this的类型是uninitializedThis;否则,实例方法中this的类型是class(N, L),其中N是包含该方法的类的名称,L是其定义的类装载器。
对于静态方法的初始类型状态来说,这是不相关的,所以这个列表是空的。
methodInitialThisType(_Class, Method, []) :-
methodAccessFlags(Method, AccessFlags)。
member(static, AccessFlags)。
methodName(Method, MethodName)。
MethodName \= '<init>' 。
methodInitialThisType(Class, Method, [This]) :-
methodAccessFlags(Method, AccessFlags)。
notMember(static, AccessFlags)。
instanceMethodInitialThisType(Class, Method, This)。
instanceMethodInitialThisType(Class, Method, class('java/lang/Object', L)) :-
methodName(Method, '<init> ')。
classDefiningLoader(Class, L)。
isBootstrapLoader(L)。
classClassName(Class, 'java/lang/Object')。
instanceMethodInitialThisType(Class, Method, uninitializedThis) :-
methodName(Method, '<init> ')。
classClassName(Class, 类名)。
classDefiningLoader(Class, CurrentLoader)。
superclassChain(ClassName, CurrentLoader, Chain)。
Chain (链)= [].
instanceMethodInitialThisType(Class, Method, class(ClassName, L)) :-
methodName(Method, MethodName)。
MethodName \= '<init>' 。
classDefiningLoader(Class, L)。
classClassName(Class, 类名)。
我们现在使用方法的初始类型状态来计算方法的合并流是否正确。
· if我们有一个堆栈地图框架和一个传入的类型状态,那么类型状态必须可以分配给堆栈地图框架中的那个。然后,我们可以继续用堆栈图框中给出的类型状态对流的其余部分进行类型检查。
· mergedCodeIsTypeSafe(Environment, [stackMap(Offset, MapFrame) | MoreCode],
· frame(Locals, OperandStack, Flags)) :-
· frameIsAssignable(frame(Locals, OperandStack, Flags), MapFrame)。
· mergedCodeIsTypeSafe(Environment, MoreCode, MapFrame)。
· if一个合并的代码流以一条相对于T来说类型安全的指令I开始,并且I满足其异常处理程序(见下文),并且在执行I之后的类型状态下,代码流的尾部是类型安全的。
NextStackFrame表示什么会落到下面的指令上。对于一个无条件的分支指令,它将有一个特殊的值afterGoto。ExceptionStackFrame表示传递给异常处理程序的内容。
mergedCodeIsTypeSafe(Environment, [ instruction(Offset, Parse) | MoreCode],
frame(Locals, OperandStack, Flags)) :-
instructionIsTypeSafe(Parse, Environment, Offset,
frame(Locals, OperandStack, Flags)。
NextStackFrame, ExceptionStackFrame)。
instructionSatisfiesHandlers(Environment, Offset, ExceptionStackFrame)。
mergedCodeIsTypeSafe(Environment, MoreCode, NextStackFrame)。
· 在一个无条件的分支之后(由传入的类型状态afterGoto表示),if我们有一个堆栈图框给出了下面指令的类型状态,我们可以继续进行,并使用堆栈图框提供的类型状态对它们进行类型检查。
· mergedCodeIsTypeSafe(Environment, [stackMap(Offset, MapFrame) | MoreCode],
· afterGoto) :-
· mergedCodeIsTypeSafe(Environment, MoreCode, MapFrame)。
· 在没有为其提供堆栈图框的情况下,在无条件分支之后有代码是非法的。
· mergedCodeIsTypeSafe(_Environment, [ instruction(_, _) | _MoreCode],
· afterGoto) :-
· Write_ln('No stack frame after unconditional branch')。
· 失败。
· if我们在代码的末尾有一个无条件的分支,就停止。
· mergedCodeIsTypeSafe(_Environment, [endOfCode(Offset)]。
· afterGoto)。
if目标有一个相关的堆栈框架Frame,并且当前的堆栈框架StackFrame可以分配给Frame,那么分支到一个目标是类型安全的。
targetIsTypeSafe(Environment, StackFrame, Target) :-
offsetStackFrame(Environment, Target, Frame)。
frameIsAssignable(StackFrame, Frame)。
if一条指令满足了适用于该指令的每个异常处理程序,那么它就满足了它的异常处理程序。
instructionSatisfiesHandlers(Environment, Offset, ExceptionStackFrame) :-
exceptionHandlers(Environment, Handlers)。
sublist(isApplicableHandler(Offset), Handlers, ApplicableHandlers)。
checklist(instructionSatisfiesHandler(Environment, ExceptionStackFrame),
适用的处理程序)。
if一条指令的偏移量大于或等于处理程序范围的起点,并且小于处理程序范围的终点,则该异常处理程序适用于该指令。
isApplicableHandler(Offset, handler(Start, End, _Target, _ClassName)) :-
偏移>=开始。
偏移量<终点。
if指令的传出类型状态是ExcStackFrame,并且处理程序的目标(处理程序代码的初始指令)假设传入类型状态T,则该指令满足异常处理程序。类型状态T是从ExcStackFrame派生出来的,方法是将操作数堆栈替换为堆栈,其唯一元素是处理程序的异常类别。
instructionSatisfiesHandler(Environment, ExcStackFrame, Handler) :-
Handler = handler(_, _, Target, _)。
currentClassLoader(Environment, CurrentLoader)。
handlerExceptionClass(Handler, ExceptionClass, CurrentLoader)。
/ 堆栈只由异常组成。/
ExcStackFrame = frame(Locals, _, Flags),
TrueExcStackFrame = frame(Locals, [ ExceptionClass ], Flags)。
operandStackHasLegalLength(Environment, TrueExcStackFrame)。
targetIsTypeSafe(环境,TrueExcStackFrame,目标)。
4.10.1.7.类型检查加载和存储指令
所有的加载指令都是一个共同模式的变化,即改变指令所加载的值的类型。
从局部变量Index加载Type类型的值是类型安全的,if该局部变量的类型是ActualType,ActualType可以赋值给Type,并且将ActualType推到传入的操作数堆栈是一个有效的类型转换(§4.10.1.4),产生一个新的类型状态NextStackFrame。在执行加载指令后,类型状态将是NextStackFrame。
loadIsTypeSafe(Environment, Index, Type, StackFrame, NextStackFrame) :-
StackFrame = frame(Locals, _OperandStack, _Flags)。
nth0(Index, Locals, ActualType)。
isAssignable(ActualType, Type)。
validTypeTransition(Environment, [], ActualType, StackFrame,
NextStackFrame)。
所有的存储指令都是一个共同模式的变化,改变指令所存储的值的类型。
一般来说,if一条存储指令所引用的局部变量的类型是Type的超类型,并且操作数堆栈的顶部是Type的子类型,其中Type是指令被设计为存储的类型,则该指令是类型安全的。
更确切地说,if可以从操作数堆栈中弹出一个与Type"匹配 "的ActualType类型(即Type的一个子类型)(§4.10.1.4),然后合法地将该类型分配给局部变量LIndex ,则该存储是类型安全的。
storeIsTypeSafe(_Environment, Index, Type,
frame(Locals, OperandStack, Flags)。
frame(NextLocals, NextOperandStack, Flags)) :-
popMatchingType(OperandStack, Type, NextOperandStack, ActualType)。
modifyLocalVariable(Index, ActualType, Locals, NextLocals)。
给定局部变量Locals,将Index修改为Type类型,会产生局部变量列表NewLocals。这种修改有些复杂,因为有些值(和它们相应的类型)占据了两个局部变量。因此,修改LN 可能需要修改LN+1 (因为类型将同时占据N和N+1槽)或LN-1 (因为本地N曾经是本地N-1开始的两个字的值/类型的上半部分,因此本地N-1必须无效),或者两者都要修改。这将在下面进一步描述。我们从L0 开始并向上计数。
modifyLocalVariable(Index, Type, Locals, NewLocals) :-
modifyLocalVariable(0, Index, Type, Locals, NewLocals)。
给出LocalsRest,即从索引I开始的局部变量列表的后缀,将局部变量Index修改为Type类型,则局部变量列表的后缀为NextLocalsRest。
ifI < Index-1,只需将输入复制到输出并向前递归。ifI = Index-1,本地I的类型可能会改变。ifLI 有一个大小为2的类型,就会发生这种情况。一旦我们将LI+1 设置为新的类型(以及相应的值),LI 的类型/值将被废止,因为它的上半部分将被废弃。然后我们向前递归。
modifyLocalVariable(I, Index, Type,
[Locals1 | LocalsRest]。
[Locals1 | NextLocalsRest] ) :-
I < 索引 - 1,
I1就是I+1。
modifyLocalVariable(I1, Index, Type, LocalsRest, NextLocalsRest)。
modifyLocalVariable(I, Index, Type,
[Locals1 | LocalsRest]。
[NextLocals1 | NextLocalsRest] ) :-
I =:=索引-1。
修改PreIndexVariable(Locals1, NextLocals1)。
modifyLocalVariable(Index, Index, Type, LocalsRest, NextLocalsRest)。
当我们找到这个变量,并且它只占一个字时,我们把它改成Type,我们就完成了。当我们找到这个变量,并且它占据了两个词时,我们把它的类型改为Type,下一个词改为top。
modifyLocalVariable(Index, Index, Type,
[_ | LocalsRest], [类型 | LocalsRest]) :-
sizeOf(Type, 1)。
modifyLocalVariable(Index, Index, Type,
[_, _ | LocalsRest], [Type, top | LocalsRest]) :-
sizeOf(Type, 2)。
我们把索引紧接在类型将被修改的局部之前的局部称为预索引变量。InputType类型的预索引变量的未来类型是结果。if预索引局部的类型,Type,大小为1,它不会改变。if预索引局部的类型,Type,是2,我们需要把它的两个字值的下半部分标记为不可用,把它的类型设置为top。
modifyPreIndexVariable(Type, Type) :- sizeOf(Type, 1).
modifyPreIndexVariable(Type, top) :- sizeOf(Type, 2).
4.10.1.8.受保护成员的类型检查
所有访问成员的指令都必须与受保护成员的规则相抗衡。本节描述了与JLS §6.6.2.1相对应的受保护检查。
受保护的检查只适用于当前类的超类中受保护的成员。其他类中受保护的成员将被解析时进行的访问检查所捕获(§5.4.4)。有四种情况。
· if一个类的名字不是任何超类的名字,它就不可能是一个超类,因此可以安全地忽略它。
· passProtectedCheck(Environment, MemberClassName, MemberName,
· MemberDescriptor, StackFrame) :-
· thisClass(Environment, class(CurrentClassName, CurrentLoader))。
· superclassChain(CurrentClassName, CurrentLoader, Chain)。
· notMember(class(MemberClassName, _), Chain)。
· ifMemberClassName与一个超类的名字相同,被解析的类可能确实是一个超类。在这种情况下,if在不同的运行时包中没有名为MemberClassName的超类有一个名为MemberName的描述符MemberDescriptor的受保护成员,那么受保护检查就不适用。
这是因为实际被解析的类要么是这些超类中的一个,在这种情况下,我们知道它要么在同一个运行时包中,并且访问是合法的;要么有关成员不受保护,检查不适用;要么它是一个子类,在这种情况下,检查无论如何都会成功;要么它是同一个运行时包中的其他类,在这种情况下,访问是合法的,检查不需要进行;要么验证器不需要将这作为一个问题,因为它将被捕获,因为解析会强制失败。
passProtectedCheck(Environment, MemberClassName, MemberName,
MemberDescriptor, StackFrame) :-
thisClass(Environment, class(CurrentClassName, CurrentLoader))。
superclassChain(CurrentClassName, CurrentLoader, Chain)。
member(class(MemberClassName, _), Chain)。
在另一个Pkg中使用受保护的成员(classesInOtherPkgWithProtectedMember)。
class(CurrentClassName, CurrentLoader)。
MemberName, MemberDescriptor, MemberClassName, Chain, [])。
· if在不同的运行时包中确实存在一个受保护的超类成员,那么就加载MemberClassName;if相关的成员不受保护,这个检查就不适用。(使用不受保护的超类成员在本质上是正确的)。
· passProtectedCheck(Environment, MemberClassName, MemberName,
· 会员描述符。
· frame(_Locals, [Target | Rest], _Flags)) :-
· thisClass(Environment, class(CurrentClassName, CurrentLoader))。
· superclassChain(CurrentClassName, CurrentLoader, Chain)。
· member(class(MemberClassName, _), Chain)。
· 在另一个Pkg中使用受保护的成员(classesInOtherPkgWithProtectedMember)。
· class(CurrentClassName, CurrentLoader)。
· MemberName, MemberDescriptor, MemberClassName, Chain, List)。
· 列表 /= []。
· loadedClass(MemberClassName, CurrentLoader, ReferencedClass)。
· isNotProtected(ReferencedClass, MemberName, MemberDescriptor)。
· 否则,使用Target类型的对象的成员需要Target可以被分配到当前类的类型。
· passProtectedCheck(Environment, MemberClassName, MemberName,
· 会员描述符。
· frame(_Locals, [Target | Rest], _Flags)) :-
· thisClass(Environment, class(CurrentClassName, CurrentLoader))。
· superclassChain(CurrentClassName, CurrentLoader, Chain)。
· member(class(MemberClassName, _), Chain)。
· 在另一个Pkg中使用受保护的成员(classesInOtherPkgWithProtectedMember)。
· class(CurrentClassName, CurrentLoader)。
· MemberName, MemberDescriptor, MemberClassName, Chain, List)。
· 列表 /= []。
· loadedClass(MemberClassName, CurrentLoader, ReferencedClass)。
· isProtected(ReferencedClass, MemberName, MemberDescriptor)。
· isAssignable(Target, class(CurrentClassName, CurrentLoader))。
ifList是Chain中名称为MemberClassName的类的集合,这些类与Class在不同的运行时包中,并且拥有名为MemberName的保护成员和MemberDescriptor的描述符,则谓词ClassInOtherPkgWithProtectedMember(Class, MemberName, MemberDescriptor, MemberClassName, Chain, List)为真。
classesInOtherPkgWithProtectedMember(_, _, _, _, [], [])。
classesInOtherPkgWithProtectedMember(Class, MemberName,
MemberDescriptor, MemberClassName,
[class(MemberClassName, L) | Tail]。
[class(MemberClassName, L) | T]) :-
differentRuntimePackage(Class, class(MemberClassName, L))。
loadedClass(MemberClassName, L, Super)。
isProtected(Super, MemberName, MemberDescriptor)。
在另一个Pkg中使用受保护的成员(classesInOtherPkgWithProtectedMember)。
Class, MemberName, MemberDescriptor, MemberClassName, Tail, T)。
classesInOtherPkgWithProtectedMember(Class, MemberName,
MemberDescriptor, MemberClassName,
[class(MemberClassName, L) | Tail]。
T) :-
differentRuntimePackage(Class, class(MemberClassName, L))。
loadedClass(MemberClassName, L, Super)。
isNotProtected(Super, MemberName, MemberDescriptor)。
在另一个Pkg中使用受保护的成员(classesInOtherPkgWithProtectedMember)。
Class, MemberName, MemberDescriptor, MemberClassName, Tail, T)。
classesInOtherPkgWithProtectedMember(Class, MemberName,
MemberDescriptor, MemberClassName,
[class(MemberClassName, L) | Tail]。
T] :-
sameRuntimePackage(Class, class(MemberClassName, L))。
在另一个Pkg中使用受保护的成员(classesInOtherPkgWithProtectedMember)。
Class, MemberName, MemberDescriptor, MemberClassName, Tail, T)。
sameRuntimePackage(Class1, Class2) :-
classDefiningLoader(Class1, L)。
classDefiningLoader(Class2, L)。
samePackageName(Class1, Class2)。
differentRuntimePackage(Class1, Class2) :-
classDefiningLoader(Class1, L1)。
classDefiningLoader(Class2, L2)。
L1\= L2。
differentRuntimePackage(Class1, Class2) :-
differentPackageName(Class1, Class2)。
4.10.1.9.类型检查指令
一般来说,指令的类型规则是相对于环境给出的,环境定义了指令发生的类和方法(§4.10.1.1),以及指令发生在方法中的偏移量Offset。该规则指出,if传入的类型状态StackFrame满足某些要求,那么。
· 该指令是类型安全的。
· 可以证明,指令正常完成后的类型状态具有NextStackFrame给出的特定形式,而指令突然完成后的类型状态则由ExceptionStackFrame给出。
一条指令突然完成后的类型状态与传入的类型状态相同,只是操作数栈是空的。
exceptionStackFrame(StackFrame, ExceptionStackFrame) :-
StackFrame = frame(Locals, _OperandStack, Flags)。
ExceptionStackFrame = frame(Locals, [], Flags).
许多指令的类型规则与其他指令的规则完全同构。if一条指令b1与另一条指令b2同构,那么b1的类型规则就与b2的类型规则相同。
instructionIsTypeSafe(Instruction, Environment, Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
instructionHasEquivalentTypeRule(Instruction, IsomorphicInstruction)。
instructionIsTypeSafe(IsomorphicInstruction, Environment, Offset,
堆栈框架,NextStackFrame。
ExceptionStackFrame)。
每条规则的英文描述旨在使其可读性、直观性和简洁性。因此,该描述避免了重复上面给出的所有背景假设。特别是。
· 说明中没有明确提到环境。
· 当描述中谈到下面的操作数栈或局部变量时,它指的是类型状态的操作数栈和局部变量组成部分:要么是传入的类型状态,要么是传出的类型状态。
· 指令突然完成后的类型状态几乎总是与传入的类型状态相同。说明中只讨论了指令突然完成后的类型状态,而事实并非如此。
· 该描述谈到了在操作数堆栈中弹出和推入类型,并没有明确讨论堆栈下溢或溢出的问题。该描述假设这些操作可以成功完成,但是操作数堆栈操作的Prolog条款确保了必要的检查。
· 该描述只讨论了逻辑类型的操作。在实践中,有些类型需要一个以上的词。该描述从这些表示细节中抽象出来,但操作数据的Prolog子句却没有。
任何不明确的地方都可以通过参考正式的Prolog条款来解决。
aaload
aaload指令是类型安全的,if可以有效地用ComponentType替换与int和数组类型相匹配的类型,其中ComponentType是Object的一个子类型,ComponentType产生出的类型状态。
instructionIsTypeSafe(aload, Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
nth1OperandStackIs(2, StackFrame, ArrayType)。
arrayComponentType(ArrayType, ComponentType)。
isBootstrapLoader(BL)。
validTypeTransition(Environment,
[int, arrayOf(class('java/lang/Object', BL))]。
ComponentType, StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
X的一个数组的分量类型是X,我们定义null的分量类型是null。
arrayComponentType(arrayOf(X), X)。
arrayComponentType(null, null)。
aastore
aastore指令是类型安全的,if可以有效地从传入的操作数栈中弹出与Object、int和Object的数组相匹配的类型,产生传出的类型状态。
instructionIsTypeSafe(aastore, _Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
isBootstrapLoader(BL)。
canPop(StackFrame,
[class('java/lang/Object', BL),
int,
arrayOf(class('java/lang/Object', BL))]。
NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
aconst_null
if可以有效地将null类型推到传入的操作数堆栈中,产生传出的类型状态,那么aconst_null指令就是类型安全的。
instructionIsTypeSafe(aconst_null, Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
validTypeTransition(Environment, [], null, StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
aload, aload_<n
if一条带有操作数Index的加载指令是类型安全的,并且产生一个出场的类型状态NextStackFrame,那么带有操作数Index和类型引用的加载指令是类型安全的,并且产生一个出场的类型状态NextStackFrame。
instructionIsTypeSafe(aload(Index), Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
loadIsTypeSafe(Environment, Index, reference, StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
指令aload_ ,对于0≤n≤3,是类型安全的,if等价的aload指令是类型安全的。
instructionHasEquivalentTypeRule(aload_0, aload(0))。
instructionHasEquivalentTypeRule(aload_1, aload(1))。
instructionHasEquivalentTypeRule(aload_2, aload(2))。
instructionHasEquivalentTypeRule(aload_3, aload(3))。
anewarray
一个具有操作数CP的anewarray指令是类型安全的,ifCP指的是一个表示类类型或数组类型的常量池条目,并且人们可以合法地在传入的操作数栈上用一个具有组件类型CP的数组来替换类型匹配的int,产生传出的类型状态。
instructionIsTypeSafe(anewarray(CP), Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
(CP = class(_, _) ; CP = arrayOf(_))。
validTypeTransition(Environment, [int], arrayOf(CP),
StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
areturn
一个areturn指令是类型安全的,if包围方法有一个声明的返回类型,ReturnType,它是一个引用类型,并且我们可以有效地从传入的操作数堆栈中弹出一个与ReturnType匹配的类型。
instructionIsTypeSafe(areturn, Environment, _Offset, StackFrame,
afterGoto, ExceptionStackFrame) :-
thisMethodReturnType(Environment, ReturnType)。
isAssignable(ReturnType, reference)。
canPop(StackFrame, [ReturnType], _PoppedStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
arraylength
一个数组长度指令是类型安全的,if我们可以有效地将传入操作数栈上的一个数组类型替换为产生传出类型状态的int类型。
instructionIsTypeSafe(arraylength, Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
nth1OperandStackIs(1, StackFrame, ArrayType)。
arrayComponentType(ArrayType, _)。
validTypeTransition(Environment, [top], int, StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
astore, astore_
if一条带有操作数Index的存储指令是类型安全的,并且产生一个出场的类型状态NextStackFrame,那么带有操作数Index和类型引用的存储指令是类型安全的,并且产生一个出场的类型状态NextStackFrame。
instructionIsTypeSafe(store(Index), Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
storeIsTypeSafe(Environment, Index, reference, StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
指令aster_ ,对于0≤n≤3,是类型安全的,if相当于aster指令是类型安全的。
instructionHasEquivalentTypeRule(astore_0, astore(0))。
instructionHasEquivalentTypeRule(astore_1, astore(1))。
instructionHasEquivalentTypeRule(astore_2, astore(2))。
instructionHasEquivalentTypeRule(astore_3, astore(3))。
athrow
if操作数堆栈的顶部与Throwable相匹配,则athrow指令是类型安全的。
instructionIsTypeSafe(athrow, _Environment, _Offset, StackFrame,
afterGoto, ExceptionStackFrame) :-
isBootstrapLoader(BL)。
canPop(StackFrame, [class('java/lang/Throwable', BL)], _PoppedStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
baload
一个baload指令是类型安全的,if人们可以有效地用int替换传入操作数栈上与int匹配的类型和一个小数组类型,产生传出的类型状态。
instructionIsTypeSafe(baload, Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :
nth1OperandStackIs(2, StackFrame, ArrayType)。
isSmallArray(ArrayType)。
validTypeTransition(Environment, [int, top], int,
StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
if一个数组类型是字节数组、布尔数组或其子类型(null),它就是一个小数组类型。
isSmallArray(arrayOf(byte))。
isSmallArray(arrayOf(boolean))。
isSmallArray(null)。
bastore
一个bastore指令是类型安全的,if可以有效地从传入的操作数栈中弹出与int、int和一个小数组类型相匹配的类型,产生传出的类型状态。
instructionIsTypeSafe(bastore, _Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
nth1OperandStackIs(3, StackFrame, ArrayType)。
isSmallArray(ArrayType)。
canPop(StackFrame, [int, int, top], NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
sipush
if等价的sipush指令是类型安全的,那么bipush指令就是类型安全的。
instructionHasEquivalentTypeRule(bipush(Value), sipush(Value))。
Caload
Caload指令是类型安全的,if可以有效地将传入操作数栈上与int和char数组相匹配的类型替换为int,产生传出的类型状态。
instructionIsTypeSafe(caload, Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
validTypeTransition(Environment, [int, arrayOf(char)], int,
StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
castore
一个castore指令是类型安全的,if可以有效地从传入的操作数栈中弹出与int、int和char数组相匹配的类型,产生传出的类型状态。
instructionIsTypeSafe(castore, _Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
canPop(StackFrame, [int, int, arrayOf(char)], NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
checkcast
ifCP指的是一个表示类或数组的常量池条目,那么带有操作数CP的checkcast指令是类型安全的,人们可以有效地将传入操作数堆栈顶部的Object类型替换为CP表示的类型,产生传出的类型状态。
instructionIsTypeSafe(checkcast(CP), Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
(CP = class(_, _) ; CP = arrayOf(_))。
isBootstrapLoader(BL)。
validTypeTransition(Environment, [class('java/lang/Object', BL)], CP,
StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
d2f, d2i, d2l
if一个d2f指令可以有效地从传入的操作数堆栈中弹出double并替换为float,得到传出的类型状态,那么这个指令就是类型安全的。
instructionIsTypeSafe(d2f, Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
validTypeTransition(Environment, [double], float,
StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
if可以有效地从传入的操作数堆栈中弹出double并替换为int,产生传出的类型状态,那么d2i指令就是类型安全的。
instructionIsTypeSafe(d2i, Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
validTypeTransition(Environment, [double], int,
StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
if可以有效地从传入的操作数堆栈中弹出double并替换为long,产生传出的类型状态,那么d2l指令就是类型安全的。
instructionIsTypeSafe(d2l, Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
validTypeTransition(Environment, [double], long,
StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
dadd
dadd指令是类型安全的,if人们可以有效地用double替换传入操作数栈中与double匹配的类型,产生传出的类型状态。
instructionIsTypeSafe(dadd, Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
validTypeTransition(Environment, [double, double], double,
StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
daload
一个daload指令是类型安全的,if可以有效地将传入操作数栈中与int和数组的double相匹配的类型替换为double,产生传出的类型状态。
instructionIsTypeSafe(daload, Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
validTypeTransition(Environment, [int, arrayOf(double)], double,
StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
dastore
一个dastore指令是类型安全的,if可以有效地从传入的操作数栈中弹出与double、int和数组相匹配的类型,产生传出的类型状态。
instructionIsTypeSafe(dastore, _Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
canPop(StackFrame, [double, int, arrayOf(double)], NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
dcmp
一个dcmpg指令是类型安全的,if人们可以有效地用int替换传入操作数堆栈中与double和double相匹配的类型,产生传出的类型状态。
instructionIsTypeSafe(dcmpg, Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
validTypeTransition(Environment, [double, double], int,
StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
if等同的dcmpg指令是类型安全的,那么dcmpl指令就是类型安全的。
instructionHasEquivalentTypeRule(dcmpl, dcmpg)。
dconst_
if可以有效地将双倍类型推到传入的操作数堆栈中,产生传出的类型状态,那么dconst_0指令就是类型安全的。
instructionIsTypeSafe(dconst_0, Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
validTypeTransition(Environment, [], double, StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
if一条dconst_1指令是类型安全的,那么相当于dconst_0指令是类型安全的。
instructionHasEquivalentTypeRule(dconst_1, dconst_0)。
ddiv
if等同的dadd指令是类型安全的,那么ddiv指令就是类型安全的。
instructionHasEquivalentTypeRule(ddiv, dadd)。
dload, dload_<n
if一条带有操作数Index的加载指令是类型安全的,并且产生一个出场的类型状态NextStackFrame,那么带有操作数Index和类型double的加载指令就是类型安全的,并且产生一个出场的类型状态NextStackFrame。
instructionIsTypeSafe(dload(Index), Environment, _Offset, StackFrame,
NextStackFrame, ExceptionStackFrame) :-
loadIsTypeSafe(Environment, Index, double, StackFrame, NextStackFrame)。
exceptionStackFrame(StackFrame, ExceptionStackFrame)。
指令dload_ ,对于0≤n≤3,是类型安全的,if等价的dload指令是类型安全的。
instructionHasEquivalentTypeRule(dload_0, dload(0))。
instructionHasEquivalentTypeRule(dload_1, dload(1))。
指令有相等的类型规则(dload_2,dload(2))。
instructionHasEquivalentTypeRule(dload_3, dload(3))。
dmul
if等价的dadd指令是类型安全的,那么dmul指令就是类型安全的。
instructionHasEquivalentTypeRule(dmul, dadd)。
dneg
dneg指令是类型安全的