【深入理解 Java 虚拟机笔记】类文件结构

5.类文件结构

由于最近十年内虚拟机以及大量建立在虚拟机之上的程序语言如雨后春笋般出现并蓬勃发展,将我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序语言选择了操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。

无关性的基石

Java 刚诞生的宣传口号:一次编写,到处运行(Write Once, Run Anywhere)。其最终实现在操作系统的应用层:Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码

字节码(ByteCode)是构成平台无关的基石。虚拟机的语言无关性也越来越被开发者所重视,JVM 设计者在最初就考虑过实现让其他语言运行在Java虚拟机之上的可能性,如今已发展出一大批在 JVM 上运行的语言,比如 Clojure、Groovy、JRuby、Jython、Scala。

实现语言无关性的基础仍是虚拟机字节码存储格式,Java 虚拟机不和包括 Java 在内的任何语言绑定,它只与 Class 文件这种特定的二进制文件格式所关联,这使得任何语言的都可以使用特定的编译器将其源码编译成 Class 文件,从而在虚拟机上运行。

【深入理解 Java 虚拟机笔记】类文件结构_第1张图片

Class 类文件的结构

Class 文件是一组以 8 个字节为基础单位的二进制流(可能是磁盘文件,也可能是类加载器直接生成的),各个数据项目严格按照顺序紧凑地排列,中间没有任何分隔符。

Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,其中只有两种数据类型:

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

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

Class 文件格式:

【深入理解 Java 虚拟机笔记】类文件结构_第2张图片

无论是无符号数还是表,当需要描述同一个类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。

写一个简单的程序来分析 Class 文件结构:

package com.chen;
public class TestClass {
   private int m;

   public int inc() {
      return m + 1;
   }
}

通过 WinHex 打开 Class 文件:

【深入理解 Java 虚拟机笔记】类文件结构_第3张图片

魔数与 Class 文件的版本

Class文件的头 4 个字节被称为魔数 (Magic Number),唯一作用是确定文件是否为一个可被虚拟机接受的 Class 文件,固定为“0xCAFEBABE”。

第 5 和第 6 个字节是次版本号(Minor Version),第 7 和第 8 个字节是主版本号(Major Version),Java 版本号是从 45 开始的,JDK 1.1 之后每个大版本发布,主版本向上加1(JDK 1.0~1.1使用了 45.0~45.3)。Java 能向下兼容之前的版本,无法运行后续的版本。我们举的例子中,0x0034 即 52,对应 JDK 1.8。

常量池

紧接着就是常量池入口,常量池可以理解为 Class 文件之中的资源仓库,是 Class 文件结构中与其他项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项之一。

由于常量池中的常量数量不固定,因此需要在常量池前放置一项 u2 类型的数据,来表示常量池容量计数值(constant_pool_count)。该值是从 1 开始的,例子的 0x0016 为十进制的 22,代表常量池中有 21 项常量,索引值范围为1~21。设计者将第 0 项常量空出来,目的是满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。

常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References):

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

Java 代码在 javac 编译时不会有“连接”这一步骤,而是在虚拟机加载 Class 文件的时候进行动态连接。所以 Class 文件不会保存各个方法、字段和最终内存布局信息,必须经过运行期转换,才能得到真正内存入口地址。当虚拟机运行时需要从常量池获取对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。

常量池中每一项常量都是一个表,JDK 1.7 中常量池共有 14 种不同的表结构数据,这些表结构开始的第一位是一个 u1 类型的标志位,代表当前常量的类型,具体如下图所示:

【深入理解 Java 虚拟机笔记】类文件结构_第4张图片

结合下图中各个表结构的说明和之前使用 javap 解析的文件内容:

【深入理解 Java 虚拟机笔记】类文件结构_第5张图片
【深入理解 Java 虚拟机笔记】类文件结构_第6张图片

例子中,第一个常量是 0x0A,属于 CONSTANT_Methodref_info 类型,0x0004,指向第四项 CONSTANT_Class_info 常量,而0x0012 指向第18项 CONSTANT_NameAndType_info 常量。

第二个常量是 0x09,对应的是 CONSTANT_Fieldref_info 类型,0x0003,指向第三项 CONSTANT_Class_info 常量,而0x0013指向19项 CONSTANT_NameAndType_info 常量。

其余的常量都可以通过类似的方法计算出来,计算机可以帮我们完成这一步,通过 javap,使用带 -verbose 参数输出的字节码内容:

【深入理解 Java 虚拟机笔记】类文件结构_第7张图片

从图中可以看出,有一些常量从来没有在代码出现过,如“I”、“V”、“”、“LineNumberTable”、“LocalVariableTable”等,这些自动生成的常量,会在字段表(field_info)、方法表(method_info)、属性表(attribute_info)引用到,用来描述一些不方便使用“固定字节”进行表述的内容。

访问标志

在常量池结束之后,接着就是两个字节的访问标志(acess_flags),用于识别一些类或者接口层次的信息。access_flags 中一共有16个标志位可以使用,当前只定义了 8 个,没有使用的一律为 0。

具体的标志位:

标志名称 标志值 含义
ACC_PUBLIC 0x00 01 是否为 Public 类型
ACC_FINAL 0x00 10 是否被声明为 final,只有类可以设置
ACC_SUPER 0x00 20 是否允许使用 invokespecial 字节码指令的新语意,invokespecial 在JDK 1.0.2 发生过改变,在 JDK 1.0.2 之后编译出来的类这个标志都必须为真
ACC_INTERFACE 0x02 00 标志这是一个接口
ACC_ABSTRACT 0x04 00 是否为 abstract 类型,对于接口或者抽象类来说,此标志值为真,其他类型为假
ACC_SYNTHETIC 0x10 00 标志这个类并非由用户代码产生
ACC_ANNOTATION 0x20 00 标志这是一个注解
ACC_ENUM 0x40 00 标志这是一个枚举

我们例子中的 TestClass 是一个普通的 Java 类,不是接口,注解或枚举,被 public 修饰但不是 final 和 abstract,所以它的 ACC_PUBLIC、ACC_SUPER 为真,而 ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM六个标志位都应该为假,所以它的 acess_flags 应该为 0x0001|0x0020=0x0021,如图,偏移地址:000000D9

在这里插入图片描述

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

这三个索引用来确定这个类的继承关系:

  • 类索引:u2 类型的数据,用于确定类的全限定名。本例子中为 0x0003,指向常量池中第3项;
  • 父类索引:u2 类型的数据,用于确定父类的全限定名。本例子中为 0x0004,指向常量池中第4项;
  • 接口索引集合:一组 u2 类型的数据的集合,用于确定实现的接口(对于接口来说就是 extend 的接口)。第一项为接口索引计数器,u2 类型的数据,用于表示索引集合的容量。本例子中为 0x0000,说明没有实现接口。

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量,包括类级变量实例级变量,但不包括**方法内部声明的局部变量,**它不会列出从父类和超类继承而来的字段,但有可能列出原本 Java 代码中不存在的字段,如在内部类中为了保持对外部类的访问性所添加指向外部类实例的字段。并且在 Java 语言中字段是无法重载的,但对于字节码来说,字段的描述符不同,字段的重名是可行的。

字段表的结构:

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

其中字段修饰符放在 access_flags 项目中,与类中 access_flags 类似,都是 u2 数据类型,可以设置的标志位和含义:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段是否为public
ACC_PRIVATE 0x0002 字段是否为private
ACC_PROTECTED 0x0004 字段是否为protected
ACC_STATIC 0x0008 字段是否为static
ACC_FINAL 0x0010 字段是否为final
ACC_VOLATILE 0x0040 字段是否为volatile
ACC_TRANSTENT 0x0080 字段是否为transient
ACC_SYNCHETIC 0x1000 字段是否为由编译器自动产生
ACC_ENUM 0x4000 字段是否为enum

ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED 最多只能三选一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口中字段必须有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。

access_flags 之后是 name_indexdescriptor_index 。它们是对常量池的引用,分别代表着字段的简单名称以及字段方法和方法的描述符。

描述符是用来描述字段的数据类型、方法的参数列表和返回值。描述符规则:

标志符 含义
B 基本数据类型 byte
C 基本数据类型 char
D 基本数据类型 double
F 基本数据类型 float
I 基本数据类型 int
J 基本数据类型 long
S 基本数据类型 short
Z 基本数据类型 boolean
V 基本数据类型 void
L 对象类型,如 Ljava/lang/Object

对于数组类型,每一维度将使用一个前置的“[”字符来描述.如一个定义为"java.lang.Stirng[][]"类型的二维数组,将被记录为:“[[Ljava/lang/Stirng;”,一个整型数组“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”。

  1. 例中第一个 u2 数据是容量计数器 fields_count,值为 0x0001,说明这类只有一个字段表数据
  2. 接下来是 acesss_flags 标志,值为 0x0002,代表修饰符的 ACC_PRIVATE 标志为真
  3. 接着是修饰字段名称的 name_index ,值为 0x0005,常量池中的第五个常量,也就是 CONSTANT_Utf8_info 类型的字符串,值为 “m”
  4. 代表字段描述符的 descriptor_index 的值为 0x0006,指向常量池的字符串"I"
  5. 所以原代码定义的字段为:private int m;

在这里插入图片描述

在 descriptor_index 之后跟随着一个属性表集合(attribute_info)用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。对于本例中的字段 m,它的属性计数器为 0,没有需要额外描述的信息,如果字段 m 的声明改为 final static int m = 1;,那就可能会存在一项 ConstantValue 的属性,其值指向常量 1。

方法表集合

和字段表类似,方法表的结构依次包括访问标志(acess_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。

volatile 和 transient 不能修饰方法,但 synchronized、native、strictfp 和 abstract 关键字可以修饰方法,所以方法表的标志位:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否为public
ACC_PRIVATE 0x0002 方法是否为private
ACC_PROTECTED 0x0004 方法是否为protected
ACC_STATIC 0x0008 方法是否为static
ACC_FINAL 0x0010 方法是否为final
ACC_SYHCHRONRIZED 0x0020 方法是否为synchronized
ACC_BRIDGE 0x0040 方法是否是有编译器产生的方法
ACC_VARARGS 0x0080 方法是否接受参数
ACC_NATIVE 0x0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为abstract
ACC_STRICTFP 0x0800 方法是否为strictfp
ACC_SYNTHETIC 0x1000 方法是否是有编译器自动产生的

在例子中:

在这里插入图片描述

第一个 u2 类型的数据(计数器)为 0x0002,代表集合有两个方法(分别为编译器添加的实例构造器和源码中的 inc())。

  1. 第一个方法的访问标志为 0x0001,即只有 ACC_PUBLIC 为真
  2. 名称索引值为 0x0007,常量池第七项,即 “”
  3. 描述符索引值为 0x0008,即 “()V”
  4. 属性表计数器为 0x0001,表示此方法属性表集合有一项属性,属性名称索引为 0x0009,对应常量池第九项 “Code”,说明此属性是方法的字节码描述。

第二个方法,inc()方法:

在这里插入图片描述

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

Java 语言中无法通过返回值不同来对一个已有方法进行重载的,但在 Class 文件结构中,只要描述符不同的两个方法也可以共存。也就是说,返回值不同,也可以 共存于同一个 Class 文件中。

属性表集合

在 Class 文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。属性表集合不要求各个属性表的严格顺序,只要不要已有属性名重复,任何人实现的编译器都可以向属性表写入自己定义的属性信息,JVM 运行时会忽略不认识的属性。

虚拟机规范预定义的属性:

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量池
Deprecated 类,方法,字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
EnclosingMethod 类文件 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClass 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
StackMapTable Code属性 JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类是否匹配
Signature 类,方法表,字段表 用于支持泛型情况下的方法签名
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 用于存储额外的调试信息
Synthetic 类,方法表,字段表 标志方法或字段为编译器自动生成的
LocalVariableTypeTable 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations 类,方法表,字段表 为动态注解提供支持
RuntimeInvisibleAnnotations 表,方法表,字段表 用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotation 方法表 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法
RuntimeInvisibleParameterAnnotation 方法表 作用与RuntimeInvisibleAnnotations属性类似,作用对象为方法参数
AnnotationDefault 方法表 用于记录注解类元素的默认值
BootstrapMethods 类文件 用于保存invokeddynamic指令引用的引导方式限定符

属性表的结构:

类型 名称 数量
u2 attribute_name_index 1
u2 attribute_length 1
u1 info attribute_length

Code 属性

Java 程序方法体中的代码经过 Javac 编译器处理后,变为字节码指令存储在 Code 属性内。并非所有方法表都存在这个属性,比如接口或抽象类中的方法。

Code 属性表结构:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_length
u2 attributes_count 1
attribute_info attributes attributes_count

本例中的第一个方法 ,对应字节码:

在这里插入图片描述

其中

  1. 0x0009 代表属性名称"Code"
  2. 0x0000002f 是属性值长度,属性值长度固定为整个属性表长度减去 6 个字节。
  3. 0x0001 代表操作数栈(Operad Stacks)深度的最大值
  4. 0x0001 代表局部变量表所需的存储空间。单位是 Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位。方法参数(包括实例方法中的"this")、显式异常处理器的参数(Exception Handler Parameter,try-catch语句中 catch 块中所定义的异常)、方法体中定义的局部变量需要局部变量表来存放。
  5. 0x00000005 存储 Java 源程序编译后的字节码指令长度。虽然是 4 字节的长度值,理论上最大值可以达到 2^32 -1,但是虚拟机规范限制了一个方法不允许超过 65535 条字节码指令。
  6. 0x2ab70001b1 是字节码指令,每个指令就是一个 u1 类型的单字节:
    1. 2a 对应指令为 aload_0,即将第 0 个 Slot 中为 reference 类型的本地变量推送到操作数栈顶
    2. b7 对应指令 invokespecial,以栈顶的 reference 类型的数据所指向对象作为方法接收者,调用此对象的实例构造器方法、private 方法或者父类方法。这个方法有一个 u2 类型的参数说明调用什么方法,它指向常量池中的一个 CONSTANT_Methodref_info 类型常量,即此方法的符号引用。
    3. 0001,这是 invokespecial 的参数,常量池 0x0001对应的常量为实例构造器 方法的符号引用
    4. b1 对应指令是 return,返回此方法,并且返回值为 void 。

通过 javap 得到另一个方法:

【深入理解 Java 虚拟机笔记】类文件结构_第8张图片

其中 args_size = 1 ,因为实例方法,都可以通过 “this” 关键字访问到此方法所属的对象。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个 Slot 位来存放对象实例的引用,方法参数值也会从 1 开始计算,这个处理只对实例方法有效。

在字节码指令之后是这个方法的显示异常处理表集合,异常表对于 Code 属性并不是必须存在的。

异常表格式:

类型 名称 数量
u2 start_pc 1
u2 end_pc 1
u2 handler_pc 1
u2 catch_type 1

这四个字段,如果当字节码在第 start_pc 行到 end_pc行(不含第 end_pc 行)出现类型为 catch_type 或者其子类的异常,则转到第 handler_pc 行继续处理,当 catch_type 的值为 0 ,代表任意异常情况都需要转到 handler_pc 处来进行处理。

编译器通过异常表来实现 Java 异常以及 finally 处理机制。

Exceptions 属性

Exceptions 属性列举出方法中基本抛出的受查异常(Checked Exception),也就是方法描述时 throws 关键字后面列举的异常,和 Code 属性里的异常表不同。

其属性表结构如下:

类型 名称 数量
u2 attribute_name_index 1
u2 attribute_lrngth 1
u2 attribute_of_exception 1
u2 exception_index_tsble number_of_exceptions

Exceptions 属性中的 number_of_exceptions 项表示方法可能抛出 number_of_exceptions 种受查异常,每一种受查异常使用一个 exception_index_table 项表示,exception_index_table 是一个指向常量池中 CONSTANT_Class_info 型常量的索引,代表了该受查异常的类型。

LineNumberTable 属性

LineNumberTable 属性用于描述 Java 源代码行号与字节码行号(字节码偏移量)的对应关系。它默认会生成在 Class 文件中,可以在 Javac 中通过 -g:none 或 -g:lines 选项来取消或生成这项信息。如果没有这个属性,运行时抛异常不会显示出错的行号,在代码调试时无法按照源码行来设置断点。

结构:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 line_number_table_length 1
line_number_info line_number_table line_number_table_length

LocalVariableTable 属性

它是用于描述栈帧中局部变量表中的变量与 Java 源码中定义的变量之间的关系,它默认会生成在 Class 文件中,可以在 Javac 中通过 -g:none 或 -g:vars 选项来取消或生成这项信息。如果没有这个属性,所有的参数名称都会丢失,取之以 arg0、arg1 这样的占位符来替代。

LocalVariableTable 结构:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 local_variable_table_length 1
local_variable_info local_variable_table local_variable_table_length

其中 local_variable_info 代表一个栈帧与源码中的局部变量的关联,结构:

类型 名称 数量 说明
u2 start_pc 1 局部变量的生命周期开始的字节码偏移量
u2 length 1 局部变量作用范围覆盖的长度
u2 name_index 1 指向常量池中 CONSTANT_Utf8_info 类型常量的索引,局部变量名称
u2 descriptor_index 1 指向常量池中 CONSTANT_Utf8_info 类型常量的索引,局部变量描述符
u2 index 1 局部变量在栈帧局部变量表中 Slot 的位置,如果这个变量的数据类型为 64 位类型(long或double),它占用的 Slot 为 index 和 index+1 这 2 个位置

SourceFile 属性

SourceFile 属性用于记录生成这个 Class 文件的源码文件名称。可以使用 Javac 的 -g:none 和 -g:source 来关闭或生成。对于大多数类来说,类名和文件名相同,但有些例外情况(如内部类)例外。如果不生成这项属性,当抛出异常,堆栈将不会显示出错代码所属的文件名。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 sourcefile_index 1

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

ConstantValue 属性

ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值。

只有被 static 关键字修饰的变量才可以用这个属性。对于非 static 类型的变量的赋值是在实例构造器 方法中进行的。而对于类变量有两种方式:在类构造器方法中或者使用 ConstantValue 属性。

目前 Sun Javac 编译器的选择是:

  • 同时使用 final 和 static 修饰的变量,并且为基本数据类型或 java.lang.String 类型使用 ConstantValue 属性初始化
  • 如果没有 final 修饰,或并非基本数据类型,则选择在 方法中初始化

虚拟机规范中并没有强制要求要求 ConstantValue 属性的字段必须设置 ACC_FINAL 标志,只是必须设置 ACC_STATIC 标志而已。对 final 关键字的要求是 Javac 编译器自己加入的限制。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 constantvalue_index 1

ConstantValue 属性是一个定长属性,其 attribute_length 数据项值必须固定为 2。constantvalue_index 代表常量池的一个字面量引用。

InnerClasses 属性

InnerClasses 属性用于记录内部类与宿主类之间的关联。

属性结构:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_classes 1
inner_classes_info inner_class number_of_classes

number_of_classes 记录多少个内部类信息,每一个内部类信息由一个 inner_classes_info 表描述,其结构:

类型 名称 数量 作用
u2 inner_classes_info_index 1 指向常量池 CONSTANT_Utf8_info 类型常量的索引,内部类的符号引用
u2 outer_classes_info_index 1 指向常量池 CONSTANT_Utf8_info 类型常量的索引,宿主类的符号引用
u2 inner_name_index 1 指向常量池 CONSTANT_Utf8_info 类型常量的索引,内部类的名称,如果是匿名内部类,则为 0
u2 inner_class_access_flags 1 内部类的访问标志

inner_class_access_flags 标志:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 内部类是否为public
ACC_PRIVATE 0x0002 内部类是否为private
ACC_PROTECTED 0x0004 内部类是否为protected
ACC_STATIC 0x0008 内部类是否为static
ACC_FINAL 0x0010 内部类是否为final
ACC_INTERFACE 0x0020 内部类是否为接口
ACC_ABSTRACT 0x0400 内部类是否为抽象类
ACC_SYNCHETIC 0x1000 字段是否为由编译器自动产生
ACC_ANNOTATION 0x2000 内部类是否为注解
ACC_ENUM 0x4000 内部类是否为枚举

Deprecated 和 Synthetic 属性

这两个都属于标志类型的布尔属性。

Deprecated 表示某个类、字段或方法,已经被程序作者定义为不再推荐使用,可以在代码通过 @deprecated 标识。

Synthetic 表示字段或方法不是由Java源码直接产生,而是编译器自行添加的,唯一例外是实例构造器 和类构造器

Deprecated 和 Synthetic 属性结构:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1

attribute_length 数据项值必须为 0x00000000,因为不需要属性值设置

StackMapTable 属性

这是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_entries 1
stack_map_frame stack_map_frame entries number_of_entries

Signature 属性

一个可选的定长属性,在 JDK 1.5 发布后增加的,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则 Signature 属性会为它记录泛型签名信息。这主要是因为 Java 的泛型采用的是擦除法实现的伪泛型,在字节码中泛型信息编译之后统统被擦除,在运行期无法将泛型类型与用户定义的普通类型同等对待。通过 Signature 属性,Java 的反射 API 能够获取泛型类型。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 signature_index 1

其中 signature_index 值必须是一个对常量池的有效索引。常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示类签名、方法类型签名或字段类型签名。

BootstrapMethods 属性

在JDK 1.7 之后加入到 Class 文件规范中。它是一个复杂的变长属性,位于类文件的属性表中,用于保存 invokedynamic指令引用的引导方法限定符。

字节码指令简介

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

加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,指令包括:

  • iload/iload 等(加载局部变量到操作栈)
  • istore/istore 等(从操作数栈存储到局部变量表)
  • bipush/sipush/ldc/iconst_(加载常量到操作数栈)
  • wide(扩充局部变量表访问索引)

其中部分指令(如 iload_),代表一组指令(如 iload_0、iload_1、iload_2、iload_3),它们都是带有一个操作数的通用指令,它们省略了显式的操作数,如 iload_0 与操作数为 0 的 iload 指令语义完全一致。

###运算指令

运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。没有直接支持 byte、short、char 和 boolean 类型的算术指令而采用 int 代替。

  • 加减乘除:iadd/isub/imul/idiv
  • 求余:irem
  • 取反:ineg
  • 位移:ishl/ishr
  • 按位或:ior
  • 按位与:iand
  • 按位异或:ixor
  • 局部变量自增:iinc
  • 比较:dcmpg/dcmpl/fcmpg/fcmpl/lcmp

类型转换指令

类型转换指令可以将两种不同的数值类型进行互相转换,这些转换操作一般用于实现用户代码中的显式类型转换操作。JVM 直接支持宽化类型转换(Widening Numeric Conversions,小范围向大范围的安全转换):

  • int 到 long、float 或 double 类型
  • long 到 float、double 类型
  • float 到 double 类型

处理窄化类型转换(Narrowing Numberic Conversions)时,必须显式使用转换指令:i2b/i2c/i2s/l2i/f2i/f2l/d2i/d2l/d2f 。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,可能会导致精度丢失。

对象创建与访问指令

虽然类实例和数组都是对象,但是虚拟机创建类对象和数组的指令是不同的。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或数组元素,指令如下:

  • 创建类实例的指令:new
  • 创建数组的指令:newarray、anewarray、multianewarray;
  • 访问类字段和实例字段的指令:getfield、putfield、getstatic、putstatic
  • 把一个数组元素加载到操作数栈的指令: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

控制转移指令

控制转移指令可以让 JVM 有条件或无条件的从指定的位置指令而不是控制转移指令的下一条指令继续执行,可以理解为控制转移指令改变了 PC 寄存器的值。指令如下:

  • 条件分支:ifeq、iflt、ifle、ifgt、ifge、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、、if_icmpge、if_acmpeq和if_acmpne
  • 复合条件分支:tableswitch、lookupswitch
  • 无条件分支:goto、goto_w、jsr、jsr_w、ret

方法调用和返回指令

这里仅仅列出 5 条用于方法调用的指令:

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

方法调用指令与类型无关,但是方法返回指令是根据返回值的类型区分的,包括 ireturn、lreturn、freturn、dreturn 和 areturn,另外还有一个 return 指令供声明为 void 的方法、实例初始化方法以及类和接口的类初始化方法使用。

异常处理指令

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

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

同步指令

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

方法级的同步是隐式的,即不需要通过字节码指令来控制,它实现在方法调用和返回操作中。虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令就会去检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置了,如果设置,执行线程就要求持有管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个方法在执行期间发生了异常,并在方法中无法处理次异常,那么这个同步方法所持有的管程将在异常抛出后自动释放。

同步一段指令集序列通常是由 Java 语言中的 synchronized 语句块表示的,Java 虚拟机的指令集中有 monitorentermonitorexit指令来支持 synchronized 关键字的语义。正确实现 synchronized 关键字需要 Javac 编译器和 Java 虚拟机两者共同协作。编译器必须保证每个 monitorenter指令都有对应的 monitorexit指令。

公有设计和私有设计

JVM 规范描述了共同程序存储格式:Class 文件格式以及字节码指令集。这些内容与硬件、操作系统以及具体的 JVM 实现之间是完全独立的,虚拟机实现者更愿意把它们看做是程序在各种 Java 平台之间互相安全的交互的手段。

Java 虚拟机的实现必须能够读取 Class 文件并精确实现包含在其中的 Java 虚拟机代码的含义。但一个优秀的虚拟机实现,在满足虚拟机规范的约束下具体实现做出修改和优化也是完全可行,甚至是被鼓励的。虚拟机后台如何处理 Class 文件并不关心,只要外部接口的表现与规范描述的一致即可。

虚拟机实现的方式主要有两种:

  • 将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集
  • 将输入的Java虚拟机代码在加载或执行时翻译宿主机 CPU 的本地指令集(即 JIT 代码生成技术)

Class 文件结构的发展

Class 文件结构一直比较稳定,主要的改进集中向访问标志、属性表这些可扩展的数据结构中添加内容。Class 文件格式所具备的平台中立、紧凑、稳定和可扩展的特点,是 Java 技术体系实现平台无关、语言无关两项特性的重要支柱。

小结

本章详细讲解了Class文件结构的各个部分,通过一个实例演示了Class的数据是如何存储和访问的,后面的章节将以动态的、运行时的角度去看看字节码在虚拟机执行引擎是怎样被解析执行的。

参考资料

  • 周志明. 深入理解Java虚拟机 : JVM高级特性与最佳实践 : Understanding the JVM : advanced features and best practices[M]. 机械工业出版社, 2013.

你可能感兴趣的:(JVM)