Class 类文件是一组以 8 字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件中,中间没有添加任何分隔符。当遇到须要占用 8 字节以上空间的数据项目时,则按照高位在前(最高位字节在地址最低位)的方式分割成若干个 8 位字节进行存储。数组
Class 文件格式采用一种相似 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。安全
不管是无符号数仍是表,当须要描述同一类型但数量不定的多个数据时,会使用一个前置的容量计数器加若干个连续数据项的形式,这若干个连续数据项称为集合。架构
Class 文件格式:ide
类型 | 名称 | 数量 |
---|---|---|
u4 | magic(魔数) | 1 |
u2 | minor_version(次版本号) | 1 |
u2 | major_version(主版本号) | 1 |
u2 | constant_pool_count(常量池容量计数器) | 1 |
cp_info | constant_pool(常量池) | constant_pool_count - 1 |
u2 | access_flags(访问标志) | 1 |
u2 | this_class(类索引) | 1 |
u2 | super_class(父类索引) | 1 |
u2 | interfaces_count(接口计数器) | 1 |
u2 | interfaces(接口索引集合) | interfaces_count |
u2 | fields_count(字段表计数器) | 1 |
field_info | fields(字段表集合) | fields_count |
u2 | methods_count(方法表计数器) | 1 |
method_info | methods(方法表集合) | methods_count |
u2 | attributes_count(属性表计数器) | 1 |
attribute_info | attributes(属性表集合) | attributes_count |
每一个 Class 文件的头 4 个字节称为魔数,用于肯定该文件是否为一个能被虚拟机接受的 Class 文件。其值为:0xCAFEBABE(咖啡宝贝?)。this
紧接着魔数的 4 个字节存储的是 Class 文件的版本号:第 五、6 个字节是次版本号,第 七、8 个字节是主版本号。编码
紧接着主次版本号以后的是常量池入口,常量池能够理解为 Class 文件中的资源仓库。线程
因为常量池中常量的数量是不固定的,因此在常量池入口放置了一个 u2 类型的常量池容量计数器。该计数器的索引值是从 1 而不是从 0 开始,当表示“不引用任何一个常量池项目”时,则可将计数器置为 0。code
常量池主要存放两大类常量:字面量和符号引用。每一项常量都是一个表,这些表开始的第一位是一个 u1 类型的标志位,表明当前常量所属的常量类型。常量池目前有 14 种常量类型,它们各自均有本身的结构。对象
常量池的项目类型:继承
类型 | 标志 | 描述 |
---|---|---|
CONSTANCT_Utf8_info | 1 | UTF-8 编码的字符串 |
CONSTANCT_Integer_info | 3 | 整型字面量 |
CONSTANCT_Float_info | 4 | 浮点型字面量 |
CONSTANCT_Long_info | 5 | 长整型字面量 |
CONSTANCT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANCT_Class_info | 7 | 类或接口的符号引用 |
CONSTANCT_String_info | 8 | 字符串类型字面量 |
CONSTANCT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANCT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANCT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANCT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANCT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANCT_MethodType_info | 16 | 标识方法类型 |
CONSTANCT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
常量类型结构:
(1)CONSTANT_Class_info 类型常量
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 标志位,值为 0x07 |
u2 | name_index | 1 | 索引值,指向常量池中一个 CONSTANT_Utf8_info 类型常量,表示这个类(或接口)的全限定名 |
(2)CONSTANT_Utf8_info 类型常量
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 标志位,值为 0x01 |
u2 | length | 1 | UTF-8 编码的字符串占用的字节数 |
u1 | bytes | length | 长度为 length 的 UTF-8 编码的字符串 |
(3)...
常量池以后,紧接着的两个字节表明访问标志,用于识别一些类或接口层次的访问信息,包括:这个 Class 是类仍是接口、是否认义为 public 类型、是否认义为 abstract 类型、是否被声明为 final(只有类可设置)等。
访问标志:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为 public 类型 |
ACC_FINAL | 0x0010 | 是否被声明为 final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否容许使用 invokespecial 字节码指令的新语意,invokespecial 指令的语意在 JDK1.0.2 发生过改变,为了区别使用哪一种语意,JDK1.0.2 以后编译出来的类的这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 标识这个一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并不是由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
类索引和父类索引都是 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,Class 文件由这三项数据肯定这个类的继承关系。
字段表用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
字段表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags(字段访问标志) | 1 |
u2 | name_index(简单名称索引) | 1 |
u2 | descriptor_index(描述符索引) | 1 |
u2 | attributes_count(属性表计数器) | 1 |
attribute_info | attributes(属性表集合) | attributes_count |
标志名称 | 标志值 | 含义 |
---|---|---|
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_TRANSIENT | 0x0080 | 字段是否 transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生的 |
ACC_ENUM | 0x4000 | 字段是否 enum |
方法表的结构与字段表同样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项,这些数据项目的含义也很是相似,仅在访问标志和属性表集合的可选项中有所区别。
方法表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags(字段访问标志) | 1 |
u2 | name_index(简单名称索引) | 1 |
u2 | descriptor_index(描述符索引) | 1 |
u2 | attributes_count(属性表计数器) | 1 |
attribute_info | attributes(属性表集合) | attributes_count |
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否 public |
ACC_PRIVATE | 0x0002 | 方法是否 private |
ACC_PROTECTED | 0x0004 | 方法是否 protected |
ACC_STATIC | 0x0008 | 方法是否 static |
ACC_FINAL | 0x0010 | 方法是否 final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否 synchronized |
ACC_BRIDGE | 0x0040 | 方法是不是由编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否 native |
ACC_ABSTRACT | 0x0400 | 方法是否 abstract |
ACC_STRICTFP | 0x0800 | 方法是否 stricftp |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动产生的 |
方法里的 Java 代码,通过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面。
在 Class 文件、字段表、方法表均可以携带本身的属性表集合,以用于描述某些场景专有的信息。
属性表不要求各个属性表具备严格的顺序,而且只要不与已有属性名重复,任何人实现的编译器均可以向属性表中写入本身定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。
属性表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
(1)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_table_length |
u2 | attritutes_count | 1 |
attribute_info | attritutes | attritutes_count |
(2)Exceptions 属性
用于列举出方法中可能抛出的受查异常,也就是方法描述时在 throws 关键字后列举的异常。
Exceptions 属性表的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exceptions | 1 |
u2 | exception_index_table | number_of_exceptions |
(3)...
Java 虚拟机的指令由一个操做码和零至多个操做数构成。因为 Java 虚拟机采用面向操做数栈而不是寄存器的架构,全部大多数指令都不包括操做数,只有一个操做码。可是大多数指令都包含了其操做所对应的数据类型信息。
若是不考虑异常处理,Java 虚拟机的解释器可使用下面的伪代码看成最基本的执行模型来理解:
do { 自动计算 PC 寄存器的值加 1; 根据 PC 寄存器的指示位置,从字节码流中取出操做码; if ( 字节码存在操做数 ) 从字节码流中取出操做数; 执行操做码所定义的操做; }
对于大多数与数据类型相关的字节码指令,它们的操做码助记符中都有特殊的字符来表示专门为哪一种数据类型服务:i 表明对 int 类型的数据操做,l 表明 long,s 表明 short,b 表明 byte,c 表明 char,f 表明 float,d 表明 double,a 表明 reference。
加载和存储指令用于将数据在栈帧中的局部变量表和操做数栈之间来回传输,这类指令包括:
以上列举的指令助记符中,有一部分是以尖括号结尾的指令。这几组指令是带有一个操做数的通用指令(如 iload)的特殊形式,它们省略了显式的操做数,而是将操做数隐含在指令中。例如:iload_0 表明操做数为 0 的 iload 指令。
运算或算术指令用于对两个操做数以上的值进行某种特定运算,并把结果从新存入到操做数栈顶。大致上算术指令可分为两种:对整型数据进行运算的指令和对浮点型数据进行运算的指令。全部的算术指令以下:
类型转换指令能够将两种不一样的数值类型进行相互转换,这些转换操做通常用于实现用户代码中的显式类型转换操做,或者用来处理字节码指令集中数据类型相关指令没法与数据类型一一对应的问题。
Java 虚拟机直接支持(即转换时无需显示的转换指令)如下数值类型的宽化类型转换(小范围类型向大范围类型的安全转换):
相对的,处理窄化类型转换时,必须显示地使用转换指令来完成,这些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f。窄化类型转换可能会致使转换结果产生不一样的正负号、不一样的数量级的状况,转换过程极可能会致使数值的精度丢失。
虽然类实例和数组都是对象,但 Java 虚拟机对类实例和数组的建立与操做使用了不一样的字节码指令。相关指令以下:
Java 虚拟机提供了一些用于直接操做操做数栈的指令,包括:
控制转移指令可让 Java 虚拟机有条件或无条件地从指定位置的指令继续执行程序,而不是从控制转移指令的下一条指令继续执行程序。从概念模型上理解,可认为控制转移指令就是在有条件或无条件地修改 PC 寄存器的值。控制转移指令以下:
方法调用指令与数据类型无关,包括:
方法返回指令是根据返回值的类型区分的,包括:ireturn(用于返回值是 boolean、byte、char、short、int 的方法)、lreturn、freturn、dreturn、areturn、return(用于 void 方法、实例初始化方法、类和接口的类初始化方法)。
Java 虚拟机中显式抛出异常的操做(throw 语句)都由 athrow 指令实现。而处理异常(catch 语句)则不是由字节码指令来实现的(好久以前曾经使用 jsr 和 ret 指令实现),而是采用异常表来完成。
Java 虚拟机能够支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
方法级的同步是隐式的,即无须经过字节码指令来控制。虚拟机能够从方法访问标志 ACC_SYNCHRONIZED 得知一个方法是否声明为同步方法。若是方法访问标志 ACC_SYNCHRONIZED 被设置为 true,执行线程就要求先成功持有管程,而后才能执行方法,最后当方法完成时释放管程。
同步一段指令集序列一般是由 synchronized 语句来表示的,Java 虚拟机的指令集中由 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字语义。