《深刻理解 Java 虚拟机》读书笔记:类文件结构

正文

1、无关性的基石

一、两种无关性

  • 平台无关性: Java 程序的运行不受计算机平台的限制,“一次编写,处处运行”。
  • 语言无关性: Java 虚拟机只与 Class 文件关联,并不关心 Class 文件的来源是何种语言。

二、无关性的实现基础

  • 各类不一样平台的虚拟机
  • 全部平台都统一使用的字节码存储格式

2、Class 类文件的结构

Class 类文件是一组以 8 字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件中,中间没有添加任何分隔符。当遇到须要占用 8 字节以上空间的数据项目时,则按照高位在前(最高位字节在地址最低位)的方式分割成若干个 8 位字节进行存储。数组

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

  • 无符号数: 基本数据类型,以 u一、u二、u四、u8 来分别表明 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数。可用来描述数字、索引引用、数量值或按照 UTF-8 编码构成字符串值。
  • 表: 由多个无符号数或其余表做为数据项构成的复合数据类型,全部表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构数据,整个 Class 文件本质上就是一张表。

不管是无符号数仍是表,当须要描述同一类型但数量不定的多个数据时,会使用一个前置的容量计数器加若干个连续数据项的形式,这若干个连续数据项称为集合架构

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

二、Class 文件的版本

紧接着魔数的 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 文件由这三项数据肯定这个类的继承关系。

  • 类索引:指向一个类型为 CONSTANT_Class_info 的类描述符常量,表示该类的全限定名。
  • 父类索引:指向一个类型为 CONSTANT_Class_info 的类描述符常量,表示父类的全限定名。
  • 接口索引集合:用于描述该类实现了哪些接口,接口索引集合的入口放置了一个 u2 类型的接口计数器,表示索引表的容量。

六、字段表集合

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

字段表结构:

类型 名称 数量
u2 access_flags(字段访问标志) 1
u2 name_index(简单名称索引) 1
u2 descriptor_index(描述符索引) 1
u2 attributes_count(属性表计数器) 1
attribute_info attributes(属性表集合) attributes_count
  • 字段访问标志(access_flags):
标志名称 标志值 含义
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
  • 简单名称索引(name_index):指向常量池中一个 CONSTANT_Utf8_info 类型常量,表明字段的简单名称。
  • 描述符索引(descriptor_index):指向常量池中一个 CONSTANT_Utf8_info 类型常量,表明字段和方法的描述符。描述符的做用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
  • 属性表集合(attributes):用于存储一些额外的信息。

七、方法表集合

方法表的结构与字段表同样,依次包括了访问标志(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
  • 方法访问标志(access_flags):
标志名称 标志值 含义
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
  • attribute_name_index:表明该属性的属性名称,是一项指向 CONSTANT_Uft8_info 型常量的索引,常量值固定为“Code”。
  • attribute_length:表明属性值的长度。
  • max_stack:表明操做数栈深度的最大值。
  • max_locals:表明局部变量表所需的存储空间。
  • code_length:表明字节码长度。
  • code:用于存储字节码指令的一系列字节流。

(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)...

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_<n>、lload、lload_<n>、fload、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)的特殊形式,它们省略了显式的操做数,而是将操做数隐含在指令中。例如:iload_0 表明操做数为 0 的 iload 指令。

二、运算指令

运算或算术指令用于对两个操做数以上的值进行某种特定运算,并把结果从新存入到操做数栈顶。大致上算术指令可分为两种:对整型数据进行运算的指令和对浮点型数据进行运算的指令。全部的算术指令以下:

  • 加法指令: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、fcmpg、fcmpl、lcmp。

三、类型转换指令

类型转换指令能够将两种不一样的数值类型进行相互转换,这些转换操做通常用于实现用户代码中的显式类型转换操做,或者用来处理字节码指令集中数据类型相关指令没法与数据类型一一对应的问题。

Java 虚拟机直接支持(即转换时无需显示的转换指令)如下数值类型的宽化类型转换(小范围类型向大范围类型的安全转换):

  • int 到 long、float、double。
  • long 到 float、double。
  • float 到 double。

相对的,处理窄化类型转换时,必须显示地使用转换指令来完成,这些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f。窄化类型转换可能会致使转换结果产生不一样的正负号、不一样的数量级的状况,转换过程极可能会致使数值的精度丢失。

四、对象建立与访问指令

虽然类实例和数组都是对象,但 Java 虚拟机对类实例和数组的建立与操做使用了不一样的字节码指令。相关指令以下:

  • 建立类实例的指令:new。
  • 建立数组的指令:newarray、anewarray、multianewarray。
  • 访问类字段和实例字段的指令:getstatic、putstatic、getfield、putfield。
  • 把一个数组元素加载到操做数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
  • 将一个操做数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
  • 取数组长度的指令:arraylength。
  • 检查类实例类型的指令:instanceof、checkcast。

五、操做数栈管理指令

Java 虚拟机提供了一些用于直接操做操做数栈的指令,包括:

  • 将操做数栈的栈顶一个或两个元素出栈:pop、pop2。
  • 复制栈顶一个或两个数值并将复制值或双份的复制值从新压入栈顶:dup、dup二、dup_x一、dup2_x一、dup_x二、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。

七、方法调用和返回指令

方法调用指令与数据类型无关,包括:

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

方法返回指令是根据返回值的类型区分的,包括: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 关键字语义。

相关文章
相关标签/搜索