Class 类文件是一组以 8 字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件中,中间没有添加任何分隔符。当遇到需要占用 8 字节以上空间的数据项目时,则按照高位在前(最高位字节在地址最低位)的方式分割成若干个 8 位字节进行存储。
Class 文件格式采用一种类似 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,会使用一个前置的容量计数器加若干个连续数据项的形式,这若干个连续数据项称为集合。
Class 文件格式:
类型 | 名称 | 数量 |
---|---|---|
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(咖啡宝贝?)。
紧接着魔数的 4 个字节存储的是 Class 文件的版本号:第 5、6 个字节是次版本号,第 7、8 个字节是主版本号。
紧接着主次版本号之后的是常量池入口,常量池可以理解为 Class 文件中的资源仓库。
由于常量池中常量的数量是不固定的,所以在常量池入口放置了一个 u2 类型的常量池容量计数器。该计数器的索引值是从 1 而不是从 0 开始,当表示“不引用任何一个常量池项目”时,则可将计数器置为 0。
常量池主要存放两大类常量:字面量和符号引用。每一项常量都是一个表,这些表开始的第一位是一个 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 关键字语义。
原文:https://www.cnblogs.com/jingqueyimu/p/12381178.html