第六章 类文件结构
6.1 概述
现代将编写的程序编译成二进制本地机器码(Native Code )已不再是唯一的选择,越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。
6.2 无关性的基石
实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java 虚拟机不和包括Java 在内的任何语言绑定,它只与“ Class 文件”这种特定的二进制文件格式所关联, Class 文件中包含了Java 虚拟机指令集和符号表以及若干其他辅助信息。基于安全方面的考虑, Java 虚拟机规范要求在Class 文件中使用许多强制性的语法和结构化约束,但任一门功能性语言都可以表示为一个能被Java 虚拟机所接受的有效的Class 文件。
Java 语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比Java 语言本身更加强大。
6.3 Class 类文件的结构
Class 文件是一组以8 位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class 文件之中,中间没有添加任何分隔符,这使得整个Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8 位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8 位字节进行存储。
Class 文件格式采用一种类似于C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8 来分别代表1 个字节、2 个字节、4个字节和8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8 编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以 ”info“,结尾。表用于描述有层次关系的复合结构的数据,整个Class 文件本质上就是一张表。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。
Class文件的数据项,无论是顺序还是数量,甚至于数据存储的字节序这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
-
魔数与Class 文件的版本
每个Class 文件的头4 个字节称为魔数( Magic Number ),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class 文件。值为:0xCAFEBABE
紧接着魔数的4 个字节存储的是Class 文件的版本号:第5 和第6 个字节是次版本号,第7 和第8 个字节是主版本号
-
常量池
主次版本号之后的是常量池人口,可以理解为Class 文件之中的资源仓库,它是 Class 文件结构中与其他项目关联最多的数据类型,也是占用Class 文件空间最大的数据项目之一,同时它还是在Class 文件中第一个出现的表类型数据项目。
在常量池的入口需要放置一项 u2 类型的数据,代表常量池容量计数值( constant_pool count ),这
个容量计数是从1 而不是0 开始的(满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任
何一个常量池项目”的含义)。 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量接近于Java 语言层面的常量概念,如文本字符串、声明为final 的常量值等。符号引用则属于编译原理方面的概念包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
在Class 文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。
常量池中每一项常量都是一个表,到JDK 1.7 共有14 种结构各不相同的表结构。但这14 种表都有一个共同的特点,就是表开始的第一位是一个ul 类型的标志位,代表当前这个常量属于哪种常量类型,这14 种常量项的结构定义如下。
-
访问标志
在常量池结束之后,紧接着的两个字节代表访问标志( access_flags ),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class 是类还是接口;是否定义为public 类型:是否定义为abs国ct 类型;如果是类的话,是否被声明为final 等。access_ flags 中一共有16 个标志位可以使用,当前只定义了其中8 个,没有使用到的标志位要求一律为0。
-
类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2 类型的数据,而接口索引集合( interfaces )是一组u2 类型的数据的集合, Class 文件中由这三项数据来确定这个类的继承关系。
- 类索引用于确定这个类的全限定名。
- 父类索引用于确定这个类的父类的全限定名。
- 接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements 语句后的接口顺序从左到右排列在接口索引集合中。
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2 类型的索引值表示,它们各自指向一个类型为 CONSTANT_Class_info 的类描述符常量,通过 CONSTANT_Class_info 类型的常量中的索引值可以找到定义在 CONSTANT_Utf8_info 类型的常量中的全限定名字符串。
对于接口索引集合,入口的第一项—u2 类型的数据为接口计数器( interfaces count),表示索引表的容量。如果该类没有实现任何接口, 则该计数器值为0,后面接口的索引表不再占用任何字节。 -
字段表集合
字段表( field_info )用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量, 但不包括在方法内部声明的局部变量。
- 字段的作用域( public 、private 、protected 修饰符)
- 是实例变量还是类变量( static 修饰符)
- 可变性( final )
- 并发可见性( volatile 修饰符,是否强制从主内存读写)
- 可否被序列化( transient 修饰符)
- 字段数据类型( 基本类型、对象、数组〉
- 字段名称。
字段修饰符放在access_flags 项目中,它与类中的access_flags 项目是非常类似的,都是一个u2 的数据类型,可以设置的标志位和含义如下:
name_index 和descriptor_index 。它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。
-
全限定名即把类全名中的“.” 替换成了“/” ,为了使连续的多个全限定名之间不产生棍淆,在使用时最后一般会加人一个“;” 表示全限定名结束。
-
简单名称即没有类型和参数修饰的方法或者字段名称。、
-
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。基本数据类型( byte 、char 、double 、float 、int 、long 、short 、boolean ) 以及代表无返回值的void 类型都用一个大写字符来表示,对象类型则用字符L 加对象的全限定名来表示,对于数组类型,每一维度将使用一个前置的“[”字符来描述。
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法
int indexOf(char[] s, int sO, int sC, char[] t, int tO, int tC, int fI)
的描述符为([CII[CIII)I
。
字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java 代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。在Java 语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。
-
方法表集合
Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式, 方法表的结构如同字段表一样。这些数据项目的含义也非常类似,仅在访问标志和属性表集合的可选项中有所区别。方法里的Java 代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“ Code ”的属性里面,属性表作为Class 文件格式中最具扩展性的一种数据项目。
与字段表集合相对应的,如果父类方法在子类中没有被重写( Override ) ,方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法, 最典型的便是类构造器“
”方法和实例构造器“ ” 方法。 -
属性表集合
属性表( attribute info )在前面的讲解之中已经出现过数次,在Class 文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
与Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写人自己定义的属性信息, Java 虚拟机运行时会忽略掉它不认识的属性,21 项预定义属性如下:
对于每个属性,它的名称需要从常量池中引用一个CONSTANT Utf8 info 类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个时的长度属性去说明属性值所占用的位数即可。
1)Code 属性
Java 程序方法体中的代码经过Javac 编译器处理后,最终变为字节码指令存储在Code属性内。Code 属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code 属性
attribute_name_index 是一项指向CONSTANT_Utf8_info 型常量的索引,常量值固定为 “Code 飞它代表了该属性的属性名称
attribute_length 指示了属性值的长度,由于属性名称索引与属性长度一共为6 字节,所以属性值的长度固定为整个属性表长度减去6 个字节。
max_stack 代表了操作数栈( Operand Stacks )深度的最大值。在方法执行的任意时刻,操作数楼都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配桔帧( StackFrame ) 中的操作战深度。
max_locals 代表了局部变量表所需的存储空间。在这里, max_locals 的单位是Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位。长度不超过3 2 位的数据类型,每个局部变量占用l 个Slot ,而double 和long 这两种64 位的数据类型则需要两个S lot 来存放。
code_length 和 code 用来存储Java 源程序编译后生成的字节码指令。code_length 代表字节码长度, code 是用于存储字节码指令的一系列字节流。code_ length 虽然是u4 类型,但是虚拟机规范中限制一个方法不允许超过65535 条字节码指令, 即它实际只使用了u2 的长度,如果超过这个限制, Javac 编译器也会拒绝编译。
Code 属性是Class 文件中最重要的一个属性,,如果把一个Java 程序中的信息分为代码和元数据两部分,那么在整个Class 文件中, Code 属性用于描述代码,所有的其他数据项目都用于描述元数据。(参考P183)
2) Exceptions 属性
Exceptions 属性是在方法表中与Code 属性平级的一项属性,作用是列举出方法中可能抛出的受查异
常( Checked Excepitons ),也就是方法描述时在throws 关键字后面列举的异常。 number_of_ exceptions 项表示方法可能抛出number_of_exceptions 种受查异常,每一种受查异常使用一个exception_index table 项表示
exception_index_table 是一个指向常量池中CONSTANT_Class_info 型常量的索引,代表了该受查异常的类型。
3)LineNumberTable 属性
LineNumberTab l e 属性用于描述Java 源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到 Class 文件之中,可以在 Javac 中分别使用 -g:none 或 -g:lines 选项来取消或要求生成这项信息。如果选择不生成LineNumberTable 属性, 对程序运行产生的最主要的影响就是当抛出异常时,堆战中将不会显示出错的行号, 并且在调试程序的时候,也无法按照源码行来设置断点。
line_number_table 是一个数量为 line_number_ table_length 、类型为line_number_info 的集合
line_number_info 表包括了 start_pc 和 line_number 两个u2 类型的数据项,前者是字节码行号,后者是Java 源码行号。
4)LocalVariableTable 属性
LocalVariableTable 属性用于描述栈帧中局部变量表中的变量与Java 源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class 文件之中, 可以在 Javac 中分别使用 -g:none 或 -g:vars 选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE 将会使用诸如 arg0 、arg1 之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。
local variable info 项目代表了一个战帧与源码中的局部变量的关联。
start_pc 和 length 属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。
name_index 和descriptor_index 都是指向常量池中CONSTANT_Utf8_info 型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。
index 是这个局部变量在梳帧局部变量表中Slot 的位置。当这个变量数据类型是64 位类型时(double 和long ),它占用的Slot 为 index 和 index+1 两个。
5)SourceFile 属性
SourceFile 属性用于记录生成这个Class 文件的源码文件名称。这个属性也是可选的,可以分别使用Javac 的 -g:none 或 -g:source 选项来关闭或要求生成这项信息。在Java 中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。
sourcefile index 数据项是指向常量池中CONSTANT Utf8 info 型常量的索引,常量值是
源码文件的文件名。6)ConstantValue 属性(定长属性)
ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值。只有被static 关键字修饰的变量(类变量〉才可以使用这项属性。如果同时使用 final 和static 来修饰一个变量,并且这个变量的数据类型是基本类型或者 java.lang.String 的话,就生成 ConstantValue 属性来进行初始化,如果这个变量没有被final 修饰,或者并非基本类型及字符串,则将会选择在
方法中进行初始化。 attribute_length 数据项值必须固定为2 。
constantvalue_index 数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是 CONSTANT_Long_ info 、CONSTANT_Float info 、CONSTANT_Double_info 、CONSTANT_Integer_info 、CONSTANT String_info 常量中的一种。
7)lnnerClasses 属性
InnerClasses 属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClas ses 属性。
数据项number_of_ clas ses 代表需要记录多少个内部类信息, 每一个内部类的信息都由一个inner_classes_info 表进行描述。
inner_class_info_index 和 outer_class_info_index 都是指向常量池中 CONSTANT_Class_info 型常量的索引,分别代表了内部类和宿主类的符号引用。
inner_name_index 是指向常量池中 CONSTANT_Utf8_info 型常量的索引,代表这个内部类的名称,如果是匿名内部类,那么这项值为0 。
inner_class_access_flags 是内部类的访问标志,类似于类的access_flags ,它的取值范围如下
8)Deprecated 及Synthetic 属性
Deprecated 属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@deprecated 注解进行设置。
Synthetic 属性代表此字段或者方法并不是由Java 源码直接产生的,而是由编译器自行添加的,在JDK 1.5 之后,标识一个类、宇段或者方法是编译器自动产生的, 也可以设置它们访问标志中的ACC_SYNTHETIC 标志位,其中最典型的例子就是Bridge Method 。所有由非用户代码产生的类、方法及字段都应当至少设置 Synthetic 属性和 ACC_SYNTHETIC 标志位中的一项,唯一的例外是实例构造器“
”方法和类构造器 “ ” 方法。 attribute_length 数据项的值必须为0x00000000,因为没有任何属性值需要设置。
9)StackMapTable 属性
StackMapTable 属性在JDK 1.6 发布后增加到了Class 文件规范中, 它是一个复杂的变长属性,位于Code 属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器( Type Checker )使用目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
在版本号大于或等于50.0 的Class 文件中,如果方法的 Code 属性中没有附带 StackMapTable 属性,那就意味着它带有一个隐式的 StackMap 属性。这个 StackMap 属性的作用等同于number_of_entries 值为0 的 StackMapTable 属性。
一个方法的Code 属性最多只能有一个StackMapTable 属性,否则将抛出ClassF ormatError 异常。
10)Signature 属性
Signature 属性在JDK 1.5 发布后增加到了Class 文件规范之中,它是一个可选的定长属性,可以出现于类、宇段表和方法表结构的属性表中。在JDK 1.5 之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量( TypeVariables )或参数化类型( Parameterized Types ),则Signature 属性会为它记录泛型签名信息。
使用这个属性去记录泛型类型的原因是:Java 语言的泛型采用的是擦除法实现的伪泛型,在字节码( Code 属性)中,泛型信息编译( 类型变量、参数化类型)之后都通通被擦除掉,但在运行期做反射时无法获得到泛型信息。Signature 属性就是为了弥补这个缺陷而增设的,现在Java 的反射API 能够获取泛型类型,最终的数据来源也就是这个属性。
signature_index 项的值必须是一个对常量池的有效索引。常量池在该索引处的项必须是CONSTANT_Utf_info 结构,表示类签名、方法类型签名或字段类型签名。
- 如果当前的 Signature 属性是类文件的属性,则这个结构表示类签名
- 如果当前的Signature 属性是方法表的属性,则这个结构表示方法类型签名
- 如果当前Signature 属性是字段表的属性,则这个结构表示字段类型签名
11)BootstrapMethods 属性
BootstrapMethods 属性在 JDK 1.7 发布后增加到了Class 文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存 invokedynamic 指令引用的引导方法限定符。,如果某个类文件结构的常量池中曾经出现过 CONSTANT_InvokeDynamic info 类型的常量,那么这个类文件的属性表中必须存在一个明确的 BootstrapMethods 属性,且类文件的属性表中最多只能有一个BootstrapMethods 属性。
目前的Javac 暂时无法生成InvokeDynamic 指令和BootstrapMethods 属性。
num_bootstrap_ methods 项的值给出了bootstrap_methods 口数组中的引导方法限定符的数量。
bootstrap_methods[] 数组的每个成员包含了一个指向常量池 CONSTANT_MethodHandle 结构的索引值它代表了一个引导方法,还包含了这个引导方法静态参数的序列。
其中引用到的bootstrap_method 结构如下:
bootstrap_methods[]数组中的每个成员必须包含下方成员
- bootstrap_ method_ref : 它值必须是一个对常量池的有效索引。常量池在该索引处的值必须是一个CONSTANT_MethodHandle_info 结构。
- num bootstrap_arguments: 它的值给出了 bootstrap_arguments[] 数组成员的数量。
- bootstrap arguments[] : 该数组的每个成员必须是一个对常量池的有效索引。常量池在该索引处必须是下列结构之一: CONSTANT_String_info 、CONSTANT_Class_info 、CONSTANT_Integer _info、CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info 、CONSTANT_MethodHandle_info或CONSTANT_MethodType_info 。
6.4 字节码指令简介
Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码Opcode ),以及跟随其后的零至多个代表此操作所需参数(称为操作数, Operands )而构成。
字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构。
缺点(导致解释执行字节码时损失一些性能。)
- 由于限制了Java 虚拟机操作码的长度为一个字节(即0 ~ 255 ),这意味着指令集的操作码总数不可能超过 256条:
- 由于C lass 文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时从字节中重建出具体数据的结构。
优点
- 放弃了操作数长度对齐e ,就意味着可以省略很多填充和间隔符号
- 用一个字节来代表操作码,也是为了尽可能获得短小精干的编译代码。
- 追求尽可能小数据量、高传输效率的设计是由Java 语言设计之初面向网络、智能家电的技术背景所决定的。
Java 虚拟机的解释器伪代码如下
do {
自动计算PC 寄存器的值加1;
根据PC 寄存器的指示位置, 从字节码流中取出操作码;
if ( 字节码存在操作数 ) 从字节码流中取出操作数;
执行操作码所定义的操作;
} while( 字节码流长度 > 0 );
-
字节码与数据类型
在Java 虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务: i 代表对int 类型的数据操作, l 代表long , s 代表short, b 代表byte, c 代表char, f 代表float, d 代表double, a代表reference。
Java 虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它,即指令集将会故意被设计成非完全独立的。有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。
Java 虚拟机所支持的与数据类型相关的字节码指令如下。
大部分的指令都没有支持整数类型byte 、char 和short,且没有指令支持 boolean 类型,编译器会在编译期或运行期将byte 和short 类型的数据带符号扩展( Sign-Extend )为相应的int 类型数据,将boolean 和char 类型数据零位扩展( Zero-Extend )为相应的 int 类型数据,对应的数组也是同理。
-
加载和存储指令
加载和存储指令用于将数据在校帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容。有一部分是以尖括号结尾的(例如
iload_<n>
,n 可以取0~3共四条指令) 。-
将一个局部变量加载到操作栈:
iload 、iload_n> 、lload 、lload_<n> 、fload 、fload_<n> 、dload 、dload_<n>、aload 、aload_<n>
-
将一个数值从操作数栈存储到局部变量表:
-
istore 、istore_<n> 、lstore 、lstore_<n> 、fstore 、fstore_<n> 、ds tore 、dstore_<n> 、a store 、astore_<n>
-
将一个常量加载到操作数樵:
bipush 、sipush 、ldc 、ldc_w 、ldc2_w 、aconst_null 、iconst_ml 、iconst_<i> 、lconst_<l>、fconst_<f>、dconst_<d>
-
扩充局部变量表的访问索引的指令: wide 。
存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作,除此之外,还有少量指令,如访问对象的字段或数组元素的指令也会向操作数栈传输数据。
-
-
运算指令
运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
对整型数据进行运算的指令与对浮点型数据进行运算的指令,无论是哪种算术指令,都使用Java 虚拟机的数据类型,由于没有直接支持byte 、short 、char 和boolean 类型的算术指令,对于这类数据的运算,应使用操作int 类型的指令代替。
算术指令
- 加法指令: iadd 、ladd 、fadd 、dadd 。
- 减法指令: isub 、lsub 、fsub 、dsub 。
- 乘法指令: imul 、lmul 、fmul 、dmul 。
- 除法指令: idiv 、ldiv 、fdiv 、ddiv 。
- 求余指令: irem 、lrem 、frem 、drem 。
- 取反指令: ineg 、lneg 、fneg 、dneg。
- 位移指令: ishl 、ishr 、iushr、lshl 、lshr 、lushr 。
- 按位或指令: ior、lor 。
- 按位与指令: iand 、land。
- 按位异或指令: ixor 、lxor。
- 局部变量自增指令: iinc。
- 比较指令: dcmpg 、dcmpl 、fempg 、fempl 、lcmp 。
数据运算可能会导致溢出,但Java 虚拟机规范没有明确定义过整型数据溢出的具体运算结果,仅规定了在处理整型数据时,只有除法指令( idiv 和 ldiv )以及求余指令(irem 和lrem )中当出现除数为零时会导致虚拟机抛出 ArithmeticException 异常,其余任何整型数运算场景都不应该抛出运行时异常。
Java 虚拟机要求在进行浮点数运算时,所有的运算结果都必须舍入到适当的精度, 非精确的结果必须舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,将优先选择最低有效位为零的(最接近数舍人模式)。
浮点数转换为整数时,所有小数部分的有效字节都会被丢弃掉。向零舍入模式将在目标数值类型中选择一个最接近但是不大于原值的数字来作为最精确的舍人结果。
Java 虚拟机在处理浮点数运算时,不会抛出任何运行时异常),当一个操作产生溢出时,将会使用有符号的无穷大来表示, 如果某个操作结果没有明确的数学定义的话,将会使用NaN 值来表示。所有使用NaN值作为操作数的算术操作,结果都会返回NaN 。
-
类型转换指令
类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作。
Java 虚拟机直接支持(即转换时无需显式的转换指令)以下数值类型的宽化类型转换:
- int 类型到long 、float 或者double 类型。
- long 类型到float 、double 类型。
- float 类型到double 类型。
处理窄化类型转换( Narrowing Numeric Conversions )时,必须显式地使用转换指令来完成,这些转换指令包括:i2b 、i2c 、i2s 、12i、f2i、f2l 、d2i 、d2l 和 d2f。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,转换过程很可能会导致数值的精度丢失。
在将 int 或 long 类型窄化转换为整数类型T 的时候,转换过程仅仅是简单地丢弃除最低位N 个字节以外的内容, N 是类型T 的数据类型长度,这将可能导致转换结果与输入值有不同的正负号。
在将一个浮点值窄化转换为整数类型T CT 限于int 或long 类型之一)的时候,将遵循以下转换规则:
- 如果浮点值是NaN,那转换结果就是int 或long 类型的0 。
- 如果浮点值不是无穷大的话, 浮点值使用IEEE 754 的向零舍入模式取整,获得整数值v,如果v 在目标类型T (int 或long )的表示范围之内,那转换结果就是 v。
- 否则,将根据v 的符号,转换为T 所能表示的最大或者最小正数。
从double 类型到 float 类型的窄化转换过程与IEEE 754 中定义的一致通过IEEE 754向最接近数舍人模式舍人得到一个可以使用float 类型表示的数字。
- 如果转换结果的绝对值大小而无法使用float 来表示的话,将返回float 类型的正负零。
- 如果转换结果的绝对值太大而无法使用float 来表示的话,将返回float 类型的正负无穷大
- 对于double 类型的NaN 值将按规定转换为float 类型的NaN 值。
Java 虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常。
-
对象创建与访问指令
虽然类实例和数组都是对象,但Java 虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素,这些指令如下。
- 创建类实例的指令: new 。
- 创建数组的指令: newarray 、anewarray 、multianewarray 。
- 访问类字段( static 字段,或者称为类变量)和实例字段(非s tatic 字段,或者称为实例变量)的指令:getfield 、putfie ld 、getstatic 、put static 。
- 把一个数组元素加载到操作数拢的指令: baload 、caload 、saload 、iaload 、laload 、faload 、daload 、aaload 。
- 将一个操作数拢的值存储到数组元素中的指令: bastore 、castore 、sastore 、iastore 、fastore 、dastore 、aastore 。
- 取数组长度的指令: arraylength 。
- 检查类实例类型的指令: instanceof、checkcast 。
-
操作数栈管理指令
如同操作一个普通数据结构中的堆楼那样, Java 虚拟机提供了一些用于直接操作操作数栈的指令,
- 将操作数拢的楼顶一个或两个元素出战: pop 、pop2 。
- 复制技顶一个或两个数值并将复制值或双份的复制值重新压入战顶: dup 、dup2 、dup_x1 、dup2_x1 、dup_x2 、dup2_x2 。
- 将战最顶端的两个数值互换: swap。
-
控制转移擂令
控制转移指令可以让Java 虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改PC 寄存器的值。
- 条件分支: ifeq 、iflt 、ifle 、ifne、ifgt 、ifge 、ifnull 、ifnonnull 、if_icmpeq 、if_icmpne 、if_icmplt 、if_icmpgt 、if_icmple 、if_icmpge 、if_acmpeq 和 if_acmpne 。
- 复合条件分支: tableswitch 、lookupswitch 。
- 无条件分支: goto 、goto_w 、jsr 、jsr_w 、ret 。
在Java 虚拟机中有专门的指令集用来处理int 和reference 类型的条件分支比较操作,为了可以无须明显标识一个实体值是否null ,也有专门的指令用来检测null 值。
对于 boolean 类型、byte 类型、char 类型和short 类型的条件分支比较操作,都是使用int 类型的比较指令来完成。
对于long 类型、float 类型和double 类型的条件分支比较操作,则会先执行相应类型的比较运算指令( dcmpg 、dcmpl 、fempg 、fempl 、lcmp),运算指令会返回一个整型值到操作数找中,随后再执行int 类型的条件分支比较操作来完成整个分支跳转。
各种类型的比较最终都会转化为int类型的比较操作,因此Java 虚拟机提供的int 类型的条件分支指令是最为丰富和强大的。
-
方法调用和返回指令
用于方法调用的5条指令如下:
- invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
- invokeinterface 指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
- invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
- invokestatic 指令用于调用类方法( static 方法) 。
- invokedynamic 指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4 条调用指令的分派逻辑都固化在Java 虚拟机内部,而invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的。
方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括 ireturn (返回值是boolean 、byte 、char 、short、int 类型时)、Ireturn 、freturn 、dreturn 和 areturn,另外还有一条return 指令供声明为void 的方法、实例初始化方法以及类和接口的类初始化方法使用。
-
异常处理指令
在Java 程序中显式抛出异常的操作( throw 语句)都由athrow 指令来实现,当然还规定了许多运行时异常会在其他 Java 虚拟机指令检测到异常状况时自动抛出。例如,除数为零时,虚拟机会在 idiv 或 ldiv 指令中抛出 ArithrneticException 异常。
在 Java 虚拟机中,处理异常( catch 语句)不是由字节码指令来实现的,而是采用异常表来完成的。 -
同步指令
Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程( Monitor )来支持的。
方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机从方法常量池的方法表结构中的ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法。
当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成时释放管程。
在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
同步一段指令集序列通常是由Java 语言中的 synchronized 语句块来表示的, Java 虚拟机的指令集中有monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义,正确实现 synchronized 关键字需要Javac 编译器与Java 虚拟机两者共同协作支持。
编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter 指令都必须执行其对应的monitorexit 指令,而无论这个方法是正常结束还是异常结束。
6.5 公有设计和私有实现
Java 虚拟机实现必须能够读取Class 文件并精确实现包含在其中的Java 虚拟机代码的语义。
虚拟机实现的方式主要有以下两种:
- 将输入的Java 虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集。
- 将输人的Java 虚拟机代码在加载或执行时翻译成宿主机CPU 的本地指令集(即JIT代码生成技术) 。
精确定义的虚拟机和目标文件格式不应当对虚拟机实现者的创造性产生太多的限制,Java 虚拟机应被设计成可以允许有众多不同的实现,并且各种实现可以在保持兼容性的同时提供不同的、新的、有趣的解决方案。
6.6 Class 文件结构的发展
相对于语言、API 以及Java 技术体系中其他方面的变化,Class 文件结构一直处于比较稳定的状态,Class文件的主体结构、字节码指令的语义和数量几乎没有出现过变动。所有对Class 文件格式的改进,都集中在向访问标志、属性表这些在设计上就可扩展的数据结构中添加内容。
Class 文件格式的发展过程中,访问标志里新加入了ACC SYNTHETIC 、ACC ANNOTATION 、ACCENUM、ACC BRIDGE 、ACC VARARGS 共5 个标志。
而属性表集合中增加了12 项新的属性,这些属性大部分用于支持Java 中许多新出现的语言特性,如枚举、变长参数、泛型、动态注解等。
Class 文件格式所具备的平台中立(不依赖于特定硬件及操作系统)、紧凑、稳定和可扩展的特点, 是Java 技术体系实现平台无关、语言无关两项特性的重要支柱。
6.7 小结
Class 文件是Java 虚拟机执行引擎的数据人口,也是 Java 技术体系的基础构成之一。了解C lass 文件的结构对后面进一步了解虚拟机执行引擎有很重要的意义。
本章详细讲解了Class 文件结构中的各个组成部分,以及每个部分的定义、数据结构和使用方法。
评论区