类文件结构

1、无关性的基石

  • 语言无关性
  • 平台无关性

各种不同的虚拟机与所有平台统一使用的程序存储格式----字节码(ByteCode),这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的一次编写,到处运行。

实现语言无关性的基础是虚拟机和字节码存储格式。Java虚拟机不和包括Java在内的任何语言绑定,只与Class文件这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。

2、Class类文件的结构

Class文件是以一组8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种类型:无符号数、表。

无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,如下图

2、1 魔数与Class文件的版本

每个Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如GIF或者JPEG等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别,主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。

魔数的值为:OxCAFEBABE。

紧接着魔数的4个字节是Class文件的版本号:5~6字节是次版本号(Minor Version),7~8字节是主版本号(Major Version)。Java版本号从45开始,高版本可以向下兼容低版本Class文件,但无法向上兼容。

2、2 常量池

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时还是在Class文件中第一个出现的表类型数据项目。

常量池入口放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count),该容量计数是从1而不是从0开始的。这样做的目的是在于满足某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。

常量池主要存放两大类常量:

  • 字面量:比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等
  • 符号引用:属于编译原理方面的概念,包括三类常量:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符

Java代码在进行javac编译时,是在虚拟机加载Class文件时进行动态连接。在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译得到具体的内存地址之中。

常量池中每一项都是一个表,在JDK1.7之前共有11中结构不相同的表结构数据,在 JDK 1.7 中为了更好地支持动态语言调用,又额外增加了3 种(CONSTANT_MethodHandle_info,CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)

  这14种表都有一个共同的特点:表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量属于哪种常量类型。

之所以说常量池是最烦锁的数据,是因为这14种常量类型各自有自己的结构。

由于Class文件中方法,字段等都需要引用CONSTAN_Utf8_info型常量来描述名称,所以CONSTAN_Utf8_info类型常量的最大长度也就是Java方法,字段名的最大长度。而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB英文字符的变量和方法名,将会无法编译。

2、3 访问标志

在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstrack类型;如果是类的话,是否被声明为final等。

2、4 类索引、父类索引与接口索引集合

   类索引(this_class)和父类索引(super_class)都是一个u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object 外,所有 Java 类的父类索引都不为 0。

接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口就按implements语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序从左到右排列在接口索引集合中。

类索引,父类索引和接口索引集合都按照顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTAN_Utif8_info类型的常量中的全限定名字符串。

  对于接口索引集合,入口的第一项——u2类型的数据为接口计数器表示索引表的容量。如果该类没有实现任何接口,则该计数值为0,后面接口的索引表不再占用任何字节。

2、5 字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。

字段可以包含的信息有:

  • 字段的作用域(public private protected)
  • 是实例变量还是类变量(static 修饰符)
  • 可变性(final)
  • 并发可见性(volatile修饰符,是否强制从主内存读写)
  • 是否可被序列化(transient 修饰符)
  • 字段数据类型(基本类型 对象 数组)
  • 字段名称

上述信息中,各个修饰符都是布尔值,适合用符号位表示,而字段叫什么名字,字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

很明显,在实际情况中,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED 三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE 不能同时选择。接口之中的字段必须有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL 标志,这些都是由 Java 本身的语言规则所决定的。

跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。

  • 全限定名

“org/fenixsoft/clazz/TestClass”是这个类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。

  • 简单名称

简单名称是指没有类型和参数修饰的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分别是“inc”和“m”。

  • 描述符

描述符的作用是用来描述字段的数据类型,方法的参数列表(包括数量,类型和顺序)和返回值。

  对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]” 类型的二维数组,将被记录为:“[[Ljava/lang/String;”,一个整型数组 “int[]” 将被记录为 “[I”。

用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()的描述符为“()V”,方法 java.lang.String toString() 的描述符为 “()Ljava/lang/String;”,方法 int indexOf(char[] source, int sourceOffset, int sourceCount, char[]target, int targetOffset, int targetCount, int fromIndex)的描述符为 “([CII[CIII)I”。

字段表的固定数据项目到decriptor_index就结束了,后面跟随着的属性表存放一些额外的信息。

字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不同的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。

2、6 方法表集合

Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。这些数据项目的含义也非常类似,仅在访问标志和属性表集合的可选项中有所区别。

因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对的,synchronized、native、strictfp 和 abstract 关键字可以修饰方法,所以方法表的访问标志中增加了 ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP 和 ACC_ABSTRACT 标志。

方法里面的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为"Code"的属性里面,属性表是Class文件格式中最具拓展性的一种数据项目。

与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器"<clinit>"方法和实例构造器“<init>”方法。

在 Java 语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名(注:Java 代码的方法特征签名只包括方法名称、参数顺序及参数类型,而字节码的特征签名还包括方法返回值以及受查异常表),特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此 Java 语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个 Class 文件中的。

2、7 属性表集合

在 Class 文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。

 对于每个属性,它的名称需要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个 u4 的长度属性去说明属性值所占用的位数即可。

1.Code属性

Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合中,但并非所有的方法都不许存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性,如果方法表有Code属性存在,那么它的结构如下图:

attribute_name_index 是一项指向 CONSTANT_Utf8_info 型常量的索引,常量值固定为“Code”,它代表了该属性的属性名称,attribute_length 指示了属性值的长度,由于属性名称索引与属性长度一共为 6 字节,所以属性值的长度固定为整个属性表长度减去 6 个字节。 

max_stack 代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。

max_locals 代表了局部变量表所需的存储空间,在这里,max_locals 的单位是 Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位。对于 byte、char、float、int、short、boolean 和 returnAddress 等长度不超过 32 位的数据类型,每个局部变量占用 1 个 Slot,而 double 和 long 这两种 64 位的数据类型则需要两个 Slot 来存放。方法参数(包括实例方法中的隐藏参数 “this”)、显式异常处理器的参数(Exception Handler Parameter,就是 try-catch 语句中 catch 块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。另外,并不是在方法中用到了多少个局部变量,就把这些局部变量所占 Slot 之和作为 max_locals 的值,原因是局部变量表中的 Slot 可以重写,当代码执行超出一个局部变量的作用域时,这个局部变量所占的 Slot 可以被其他局部变量所使用,Javac 编译器会根据变量的作用域来分配 Slot 给各个变量使用,然后计算出 max_locals 的大小。

code_length 和 code 用来存储 Java 源程序编译后生成的字节码指令。code_length 代表字节码长度,code 是用于存储字节码指令的一系列字节流。既然叫字节码指令,那么每个指令就是一个 u1 类型的单字节,当虚拟机读取到 code 中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道到这条指令后面是否需要跟随参数,以及参数应当如何理解。我们知道一个 u1 数据类型的取值范围为 0x00 ~ 0xFF,对应十进制的 0 ~ 255,也就是一共可以表达 256 条指令

 关于 code_length,有一件值得注意的事情,虽然它是一个 u4 类型的长度值,理论上最大值可以达到 2^23-1,但是虚拟机规范中明确限制了一个方法不允许超过65535 条字节码指令,即它实际只使用了 u2 的长度,如果超过这个限制,Javac 编译器也会拒绝编译。一般来讲,编写 Java 代码时只要不是刻意去编写一个超长的方法来为难编译器,是不太可能超过这个最大值的限制。但是,某些特殊情况,例如在编译一个很复杂的 JSP 文件时,某些 JSP 编译会把 JSP 内容和页面输出的信息归并于一个方法之中,就可能因为方法生成字节码超长的原因而导致编译失败。

Java虚拟机执行字节码是基于的体系结构。

在任何实例方法里面,都可以通过“this” 关键字访问到此方法所属的对象。这个方法机制对 Java 程序的编写很重要,而它的实现却非常简单,仅仅是通过 javac 编译器编译的时候把对 this 关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个 Slot 位来存放对象实例的引用,方法参数值从 1 开始计算。

在字节码指令之后的是这个方法显示异常处理表(下文简称异常表)集合,异常表对于 Code 属性来说并不是必须存在的,结构如下:

异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。

异常执行路径:

  1. 如果try语句块中出现属于Exception或其子类的异常,则转到catch语句块处理。
  2. 如果try语句块中出现不属于Exception或其子类的异常,则转到finally语句块处理。
  3. 如果catch语句块中出现任何异常,则转到finally语句块处理。

2.Exceptions属性

Exceptions 属性的作用是列举出方法中可能抛出的受检查异常(Checked Exceptions),也就是方法描述时在 throws 关键字后面列举的异常。

3.LineNumberTable属性

ineNumberTable 属性用于描述 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 两个。

在 JDK 1.5 引入泛型之后,LocalVariableTable 属性增加了一个 “姐妹属性”:LocalVariableTypeTable,这个新增的属性结构与 LocalVariableTable 非常相似,仅仅是把记录的字段描述符的 descriptor_index 替换成了字段的特征签名(Signature),对于非泛型类型来说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能准确地描述泛型类型了,因此出现了 LocalVariableTypeTable。

5.SourceFile属性

 SourceFile 属性用于记录生成这个 Class 文件的源码文件名称。这个属性也是可选的,可以分别使用 javac 的 -g:none 或 -g:source 选项来关闭或要求生成这项信息。在 Java 中,对于大多数的类来说,类名和文件名是一致的,但是又一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。这个属性是一个定长的属性。

sourcefile_index 数据项是指向常量池中 CONSTANT_Utf8_info 型常量的索引,常量值是源码文件的文件名。

6.ConstantValue属性

ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值。只有被 static 关键字修饰的变量(类变量)才可以使用这项属性。类似 “int x = 123” 和 “static int x = 123” 这样的变量定义在 Java 程序中是非常常见的事情,但虚拟机对这两种变量赋值的方式和时刻都有所不同。对于非 static 类型的变量(也就是实例变量)的赋值是在实例构造器 <init> 方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器 <clinit> 方法中或者使用 ConstantValue 属性。目前 Sun Javac 编译器的选择是:如果同时使用 final 和 static 来修饰一个变量(按照习惯,这里称 “常量” 更贴切),并且这个变量的数据类型是基本类型或者 java.lang.String 的话,就生成 ConstantValue 属性来进行初始化,如果这个变量没有被 final 修饰,或者并非基本类型及字符串,则将会选择在 <clinit> 方法中进行初始化。

ConstantValue 属性是一个定长属性,它的 attribute_length 数据项值必须固定为 2。constantvalue_index 数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一种。

7.InnerClass属性

InnerClass 属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成 InnerClass 属性。

 数据项 number_of_classes 代表需要记录多少个内部类信息,每一个内部类的信息都由一个 inner_classes_info 表进行描述。

inner_class_info_index 和 outer_class_info_index 都是指向常量池中 CONSTANT_Class_info 型常量的索引,分别代表了内部类和宿主类的符号引用。

inner_name_inex 是指向常量池中 CONSTANT_Utf8_info 型常量的索引,代表这个内部类的名称,如果是匿名内部类,那么这项值为 0。

inner_class_access_flags 是内部类的访问标志,类似于类的 access_flags。

8.Deprecated及Synthetic属性

Deprecated 和 Synthetic 两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。

        Deprecated 属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用 @deprecated 注释进行设置。

        Synthetic 属性代表此字段或者方法并不是由 Java 源码直接产生的,而是由编译器自行添加的,在 JDK 1.5 之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们访问标志中的 ACC_SYNTHETIC 标志位,其中最典型的例子就是 Bridge Method。所有由非用户代码产生的类、方法及字段都应当至少设置 Synthetic 属性和 ACC_SYNTHETIC 标志位中的一项,唯一的例外是实例构造器 “<init>” 方法和类构造器 “<clinit>” 方法。

其中attribute_length数据项的值必须为0x00000000,因为没有任何属性值需要设置。

9.StackMapTable属性

StackMapTable 属性在 JDK 1.6 发布后增加到了 Class 文件规范中,它是一个复杂的变长属性,位于 Code 属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。类型推导---->类型检查

        这个类型检查验证器在同样能保证 Class 文件合法性的前提下,省略了在运行期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而是在编译阶段将一系列的验证类型(Verification Types)直接记录在 Class 文件之中,通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能。这个验证器在 JDK 1.6 中首次提供,并在 JDK 1.7 中强制代替原本基于类型推断的字节码验证器。

     StackMapTable 属性中包含零至多个栈映射帧(Stack Map Frames),每个栈帧映射帧都显示或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。

如果方法的 Code 属性中没有附带 StackMapTable 属性,那就意味着它带有一个隐式的 StackMap 属性。这个 StackMap 属性的作用等同于 number_of_entries 值为 0 的 StackMapTable 属性。一个方法的 Code 属性最多只能有一个 StackMapTable 属性,否则将抛出 ClassFormatError 异常。

10.Signature属性

 Signature 属性记录泛型签名信息。 Java 语言的泛型采用的是擦除法实现的伪泛型在字节码(Code 属性)中,泛型信息编译(类型变量、参数化类型)之后都通通被擦除掉

使用擦除法的好处是实现简单(主要修改 Javac 编译器,虚拟机内部只做了很少的改动)、非常容易实现 backport,运行期也能够节省一些类型所占的内存空间。但坏处是运行期就无法像 C# 等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得泛型信息。Signature 属性就是为了弥补这个缺陷而增设的,现在 Java 的反射 API 能够获取泛型类型,最终的数据来源也就是这个属性。

 其中 signature_index 项的值必须是一个对常量池的有效索引。常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示类签名、方法类型签名或字段类型签名。如果当前 Signature 属性是类文件的属性,则这个结构表示类签名,如果当前的 Signature 属性是方法表的属性,则这个结构表示方法类型签名,如果当前 Signature 属性是字段表的属性,则这个结构表示字段类型签名。

11.BootstrapMethods属性

BootstrapMethods 属性在 JDK 1.7 发布后增加到了 Class 文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存 invokedynamic 指令引用的引导方法限定符。(调用动态方法)

3、字节码指令简介

Java虚拟机指令是由(占用一个字节长度、代表某种特定操作含义的数字)操作码Opcode,以及跟随在其后的零至多个代表此操作所需参数的称为操作数 Operands 构成的。由于Java虚拟机是面向操作数栈而不是寄存器的架构,所以大多数指令都只有一个操作码,而没有操作数。

字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构:

  • 由于限定了Java虚拟机操作码的长度为1个字节,指令集的操作码不能超过256条。
  • Class文件格式放弃了编译后代码中操作数长度对齐,这就意味者虚拟机处理那些超过一个字节数据的时候,不得不在运行的时候从字节码中重建出具体数据的结构。

这种操作在一定程度上会降低一些性能,但这样做的优势也非常的明显:

  1. 放弃了操作数长度对齐,就意味着可以省略很多填充和间隔符号
  2. 用一个字节来表示操作码,也是为了获取短小精悍的代码

Java虚拟机解释器可以使用下面这个伪代码当做最基本的执行模型来理解:

do  {
	计算PC寄存器的值+1;
	根据PC寄存器只是位置,从字节码流中取出操作码;
	if(存在操作数) 从字节码中取出操作数;
	执行操作码定义的操作;
} while (字节码长度>0);

3、1 字节码与数据类型

在Java虚拟机指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如,iload 指令用于从局部变量表中加载int 型的数据到操作数栈中。

由于虚拟机操作码长度只有一个字节,所以包含了数据类型的操作码就为指令集的设计带来了很大的压力:如果每一种数据类型相关的指令都支持Java虚拟机所有运行时数据类型的话,那指令集的数据就会超过256个了。因此虚拟机只提供了有限的指令集来支持所有的数据类型。即并非每种数据类型和每一种操作都有对应的指令,有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。

如load 操作, 只有iload、lload、fload、dload、aload用来支持int、long、float、double、reference 类型的入栈,而对于boolean 、byte、short 和char 则没有专门的指令来进行运算。

编译器会在编译期或运行期将byte 和 short 类型的数据带符号扩展为int类型的数据,将boolean 和 char 类型的数据零位扩展为相应的int 类型数据。与之类似,在处理boolean、byte、short 和 char 类型的数组时,也会发生转换。因此,大多数对于boolean、byte、short 和char 类型数据的操作,实际上都是使用相应的int 类型作为运算类型。

3、2 加载和存储指令

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输。

  •         将一个局部变量加载到操作数栈的指令包括:iload,iload_<n>,lload、lload_<n>、float、 fload_<n>、dload、dload_<n>,aload、aload_<n>。
  •         将一个数值从操作数栈存储到局部变量表的指令:istore,istore_<n>,lstore,lstore_<n>,fstore,fstore_<n>,dstore,dstore_<n>,astore,astore_<n>
  •         将常量加载到操作数栈的指令:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_<i>,lconst_<l>,fconst_<f>,dconst_<d>
  •         局部变量表的访问索引指令:wide

一部分以尖括号结尾的指令代表了一组指令,如iload_<i>,代表了iload_0,iload_1等,这几组指令都是带有一个操作数的通用指令。

3、3 运算指令

算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。

  •         加法指令:iadd,ladd,fadd,dadd
  •         减法指令:isub,lsub,fsub,dsub
  •         乘法指令:imul,lmul,fmul,dmul
  •         除法指令:idiv,ldiv,fdiv,ddiv
  •         求余指令:irem,lrem,frem,drem
  •         取反指令:ineg,leng,fneg,dneg
  •         位移指令:ishl,ishr,iushr,lshl,lshr,lushr
  •         按位或指令:ior,lor
  •         按位与指令:iand,land
  •         按位异或指令:ixor,lxor
  •         局部变量自增指令:iinc
  •         比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp

Java虚拟机没有明确规定整型数据溢出的情况,仅规定了处理整型数据时,只有除法(idiv 和 ldiv指令)和求余指令出现除数为0时会导致虚拟机抛出ArithmeticException异常,其余任何整形数运算场景都不应该抛出运行时异常。

Java虚拟机要求在浮点数运算的时候,所有结果否必须舍入到适当的精度,如果有两种可表示的形式与该值一样,会优先选择最低有效位为零的。称之为向最接近数舍入模式。

浮点数向整数转换的时候,Java虚拟机使用IEEE 754标准中的向零舍入模式,这种模式舍入的结果会导致数字被截断,所有小数部分的有效字节会被丢掉。

另外,Java虚拟机在处理浮点数运算时,不会抛出任何运行时异常,当一个操作产生溢出时,将会使用有符号的无穷大来表示,如果某个操作没有明确的数学定义的话,将会使用NaN值来表示。所有使用NaN值作为操作数的算术操作,都会返回NaN。

3、4 类型转换指令

类型转换指令将两种Java虚拟机数值类型相互转换,这些操作一般用于实现用户代码的显式类型转换操作。

JVM直接就支持宽化类型转换(小范围类型向大范围类型转换):

  •         int类型到long,float,double类型
  •         long类型到float,double类型
  •         float到double类型

但在处理窄化类型转换时,必须显式使用转换指令来完成,这些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和 d2f。

将int 或 long 窄化为整型T的时候,仅仅简单的把除了低位的N个字节以外的内容丢弃,N是T的长度。这有可能导致转换结果与输入值有不同的正负号。

尽管数据类型窄化转换可能会发生上限溢出,下限溢出和精度丢失等情况,但是Java虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常。

3、5 对象创建与访问指令

虽然类实例和数组都是对象,Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。

  •         创建实例的指令:new
  •         创建数组的指令:newarray,anewarray,multianewarray
  •         访问字段指令:getfield,putfield,getstatic,putstatic
  •         把数组元素加载到操作数栈指令:baload,caload,saload,iaload,laload,faload,daload,aaload
  •         将操作数栈的数值存储到数组元素中执行:bastore,castore,castore,sastore,iastore,fastore,dastore,aastore
  •         取数组长度指令:arraylength JVM支持方法级同步和方法内部一段指令序列同步,这两种都是通过moniter实现的。
  •         检查实例类型指令:instanceof,checkcast

3、6 操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,Java 虚拟机提供了一些用于直接操作操作数栈的指令,包括:

  •         将操作数栈的栈顶一个或两个元素出栈:pop、pop2
  •         复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
  •         将栈最顶端的两个数值互换:swap

3、7 控制转移指令

让JVM有条件或无条件从指定指令而不是控制转移指令的下一条指令继续执行程序。控制转移指令包括:

  •         条件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnotnull,if_cmpeq,if_icmpne,if_icmlt,if_icmpgt等
  •         复合条件分支:tableswitch,lookupswitch
  •         无条件分支:goto,goto_w,jsr,jsr_w,ret

JVM中有专门的指令集处理int和reference类型的条件分支比较操作,为了可以无明显标示一个实体值是否是null,也有专门的指令检测null 值。

boolean类型和byte类型,char类型和short类型的条件分支比较操作,都使用int类型的比较指令完成,而 long,float,double条件分支比较操作,由相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件比较操作完成整个分支跳转。各种类型的比较都最终会转化为int类型的比较操作。

3、8 方法调用和返回指令

  • invokevirtual指令:调用对象的实例方法,根据对象的实际类型进行分派(虚拟机分派)。
  • invokeinterface指令:调用接口方法,在运行时搜索一个实现这个接口方法的对象,找出合适的方法进行调用。
  • invokespecial:调用需要特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法
  • invokestatic:调用类方法(static)
  • invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

方法返回指令是根据返回值的类型区分的,包括ireturn(返回值是boolean,byte,char,short和 int),lreturn,freturn,drturn和areturn,另外一个return供void方法,实例初始化方法,类和接口的类初始化i方法使用。

3、9 异常处理指令

在Java程序中显式抛出异常的操作(throw语句)都有athrow 指令来实现,除了用throw 语句显示抛出异常情况外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。

在Java虚拟机中,处理异常不是由字节码指令来实现的,而是采用异常表来完成的。

3、10 同步指令

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。

方法级的同步是隐式的,无需通过字节码指令来控制,它实现在方法调用和返回操作中。虚拟机从方法常量池中的方法标结构中的 ACC_SYNCHRONIZED标志区分是否是同步方法。方法调用时,调用指令会检查该标志是否被设置,若设置,执行线程持有moniter,然后执行方法,最后完成方法时释放moniter。

在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。

同步一段指令集序列,通常由synchronized块标示,JVM指令集中有monitorenter和monitorexit来支持synchronized语义。

结构化锁定是指方法调用期间每一个monitor退出都与前面monitor进入相匹配的情形。JVM通过以下两条规则来保证结结构化锁成立(T代表一线程,M代表一个monitor):

  •         T在方法执行时持有M的次数必须与T在方法完成时释放的M次数相等
  •         任何时刻都不会出现T释放M的次数比T持有M的次数多的情况

为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,目的就是用来执行 monitorexit 指令。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值