类文件结构及字节码指令

类文件结构及字节码指令

1. Class类文件的结构

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

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

表是由多个无符号数、其他表作为数据项构成的复合数据类型。所有表都习惯性以“_info”结尾。

每个Class文件对应一个ClassFile结构。Class文件中字节序为Big-Endian。

ClassFile结构
ClassFile {
    u4             magic; //Class 文件的标志
    u2             minor_version;//Class 的小版本号
    u2             major_version;//Class 的大版本号
    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//Class 的访问标记
    u2             this_class;//当前类
    u2             super_class;//父类
    u2             interfaces_count;//接口数量
    u2             interfaces[interfaces_count];//一个类可以实现多个接口
    u2             fields_count;//Class 文件的字段数量
    field_info     fields[fields_count];//一个类会可以有个字段
    u2             methods_count;//Class 文件的方法数量
    method_info    methods[methods_count];//一个类可以有个多个方法
    u2             attributes_count;//此类的属性表中的属性数
    attribute_info attributes[attributes_count];//属性表集合
}
类文件字节码结构组织示意图

以下,按照在ClassFile中出现的顺序讲解。

1.1 魔数、Class文件的版本

魔数(majic)的作用是确定这个文件是否为一个能被虚拟机接受的Class文件。魔数为0xCAFEBABE。

    u4             magic; //Class 文件的标志
    u2             minor_version;//Class 的小版本号
    u2             major_version;//Class 的大版本号

第5、6个字节是次版本号(Minor Version)

第7、8个字节是主版本号(Major Version)

Java版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1。

JDK版本号:

  • J2SE 8 = 52
  • J2SE 7 = 51
  • J2SE 6.0 = 50
  • J2SE 5.0 = 49
  • JDK 1.4 = 48
  • JDK 1.3 = 47
  • JDK 1.2 = 46
  • JDK 1.1 = 45

1.2 常量池

constant_pool_count、constant_pool[constant_pool_count-1]

常量池可以理解为Class文件中的资源仓库。

常量池的容量计数是从1开始的。指向常量池的索引值需要表达“不引用任何一个常量池项目”时,将索引值设置为0。

常量池主要存放两大类常量:字面量(文本字符串、声明为 final 的常量值等)、符号引用(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)。

常量池中的所有项具有如下通用格式:

cp_info{
    u1 tag;
    u1 info[];
}

其中,tag具有下面值及其表示的含义:

类文件常量类型
类型 标志(tag) 描述
CONSTANT_utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 长整型字面量
CONSTANT_Double_info 双精度浮点型字面量
CONSTANT_Class_info 类或接口的符号引用
CONSTANT_String_info 字符串类型字面量
CONSTANT_Fieldref_info 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MothodType_info 16 标志方法类型
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

常量池中的CONSTANT_Utf8,其值采用UTF8缩略编码表示,\u0001—\u007F,用一个字节表示,\u0080—07FF,使用两个字节表示,\u0800—\uFFFF,用三个字节表示。

CONSTANT_Utf8_info结构:

CONSTANT_Utf8_info{
    u1 tag;
    u2 length;
    u1 bytes[];
}

CONSTANT_Class_info结构:

CONSTANT_Class_info{
    u1 tag;
    u2 name_index;
}

1.3 访问标志

access_flags

访问标志

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

this_class、super_class、interfaces_count、interfaces[interfaces_count]

Class文件中由这三项数据来确定这个类的继承关系。

类索引用于确定这个类的全限定名。

父类索引用于确定这个类的父类的全限定名。

接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口索引集合中。如果这个类本身是一个接口,则用extends可以继承多个接口,这些被继承的接口也放在这个接口索引集合中。

用this_class举例:

this_class指向常量池中一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info中的索引值(name_index)可以找到定义在CONSTANT_Utf8_info常量中的全限定名字符串。

1.5 字段表集合

fields_count、fields[fields_count]

字段表集合用来描述类、接口中定义的变量,包括类变量、实例变量,不包括在方法内部声明的局部变量。

字段表集合中不会列出从父类、父接口中继承而来的字段。但有可能列出原本Java代码中不存在的字段,如内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

fields_info结构如下:

fields_info结构
  1. access_flags

    access_flags
  2. descriptor_index

    表示字段的描述符。在下面1.6的方法表集合章节中,也会出现表示方法的描述符,下面一起讲解。

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

    描述符规则是:

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

    对于数组,每一维度使用一个[描述,如String[][],用[[Ljava/lang/String;表示。

    描述方法时,按照先参数列表,再返回值的顺序描述,参数列表严格按照参数顺序放在()中,如int index(char c)描述为(C)I。

  3. attribute_info

    用来描述字段的额外信息,如字段的初始值等。

1.6 方法表集合

methods_count、methods[methods_count]

method_info结构如下:

method_info
  1. access_flags

    access_flags2
  2. descriptor_index

    参见上面1.5字段表集合中的descriptor_index。

  3. attribute_info

    描述方法的额外信息,如方法体代码等。

    方法体代码编译成字节码后,存放在该方法属性表集合(也就是该attribute_info)中的Code属性里。见1.7属性表集合中的Code属性。

    如果子类没有重写父类的方法,方法表集合中不会出现来自父类的方法。但可能出现由编译器自动为类添加的方法,如类构造器、实例构造器

1.7 属性表集合

attributes_count、attributes[attributes_count]

Class文件、字段表结合、方法表集合中都有属性表集合。

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

attribute_info通用的结构:

attribute_info

attribute_name_index是引用常量池中的一个常量,属性的结构是完全自定义的,通过attribute_length说明长度,info给出属性值。

虚拟机预定义的属性:

  1. Code

    Code属性表的结构:

    Code属性表的结构

    max_stack代表了栈帧中操作数栈的最大深度。

    max_locals代表了局部变量表所需的存储空间,单位是Slot。

    Slot是虚拟机为局部变量分配内存的最小单位:对于byte、char、float、int、short、boolean、returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot;对于double、long这两种64位的数据类型需要两个Slot存放。

    局部变量表中的Slot可以重用,因此max_locals并不是所有局部变量所占Slot之和。

    code_length、code[code_length]存储Java源代码编译后生成的字节码指令。

    字节码指令,每个指令就是一个u1的单字节。u1范围是0—255,Java 虚拟机已经定义了200多条指令。

    code_length理论上最大为2^32-1,但是虚拟机规范明确限制一个方法不允许超过65535条字节码指令。

    Java在每段可能分支路径之后,都将finally语句块冗余一遍实现finally语义。

  2. Exceptions

    列举方法中可能抛出的受检异常(非RuntimeException),也就是方法描述时在throws后面列举的异常。

  3. LineNumberTable

    用于描述Java源码行号与字节码行号之间的对应关系。

    javac使用-g:none、-g:lines取消、生成这项信息。

    如果不生成LineNumberTable属性,对程序运行最主要的影响是抛出异常时,堆栈中不会显示出错的行号,在调试程序的时候,无法按照源码行设置断点。

  4. LocalVaribleTable

    用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。

    javac使用-g:none、-g:vars取消、生成这项信息。

    如果不生成LocalVaribleTable,最大的影响是别人引用这个方法时,所有的参数名称都会丢失,IDEA会使用arg0、arg1占位符代替。

  5. LocalVaribleTypeTable

    descriptor_index替换成Signature,用来描述泛型类型。

  6. SourceFile

    用来记录生成这个Class文件的源码文件名称。

    javac使用-g:none、-g:source取消、生成这项信息。

    如果不生成这项属性,当抛出异常时,堆栈中不会显示出错代码所属的文件名。

  7. ConstantValue

    通知虚拟机自动为静态变量赋值,只有static修饰的变量才可以使用这个属性。

    类变量赋值的方式:类构造器、ConstantValue属性。

    Sun Javac编译器:如果同时使用final、static修饰一个变量,并且这个变量的数据类型是基本类型、java.lang.String,生成ConstantValue属性进行初始化;如果这个变量没有被final修饰,或者并非基本类型、java.lang.String,会选择进行初始化。

  8. InnerClasses

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

  9. Deprecated

    用于类、字段、方法,表示被标注的部分不再推荐使用,在代码中使用@deprecated注解产生。

  10. Synthetic

    表示字段、方法不是由Java源码直接产生的,而是由编译器自行添加的。

    所有由非用户代码产生的类、方法、字段都应当至少设置Synthetic属性或ACC_SYNTHETIC标志中的一项,唯一的例外是

  11. StackMapTable

    在虚拟机类加载的字节码验证阶段被新类型检查验证器使用。

  12. Signature

    用于记录泛型签名信息。

    Java反射能获取到泛型类型,最终数据来源就是这个属性。

  13. BootstrapMethods

    用于保存invokedynamic指令引用的引导方法限定符。

2. 字节码指令

Java虚拟机指令 = 操作码(1个字节)+ 操作数(0个至多个)

Java虚拟机指令集中,大多数指令都包含了其操作所对应的数据类型信息。i代表int,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。

大部分指令没有支持byte、char、short、boolean类型。编译器会在编译期或运行期将byte、short数据带符号扩展为相应int,将boolean、char数据零扩展为相应int。

2.1 加载、存储指令

  • 局部变量加载到操作数栈:load、load_
  • 操作数栈存储到局部变量表:store、store_
  • 常量加载到操作数栈:ipush、ldc、const、*const_
  • 扩充局部变量表的访问索引:wide

2.2 运算指令

运算指令用于对操作数栈顶的两个值操作,再将结果重新存入到操作数栈顶。

add、sub、mul、div、rem(取余)、neg(取反)、shr、shl(左右移位)、or(按位或)、and(按位与)、xor(按位异或)、iinc(局部变量自增)、cmp(比较)

2.3 类型转换指令

i2c、i2b、i2s、l2i、f2i、f2l、d2i、d2l、d2f

2.4 对象创建、访问

  • 创建类实例:new
  • 创建数组:newarray、anewarray、multianewarray
  • 访问类变量:getstatic、putstatic
  • 访问实例变量:getfield、putfield
  • 数组元素加载到操作数栈:*aload
  • 操作数栈值存储到数组元素:*astore
  • 取数组长度:arraylength
  • 检查类实例类型:instanceof、checkcast

2.5 操作数栈管理指令

  • 将操作数栈顶一个、两个元素出栈:pop、pop2
  • 复制栈顶一个、两个数值,并将复制的值重新压入栈顶:dup_、dup2_
  • 将栈顶两个数值互换:swap

2.6 控制转移指令

  • 条件分支:if、if_icmp
  • 复合条件分支:tableswitch、lookupswitch
  • 无条件分支:goto、jsr、ret

2.7 方法调用指令

  • invokevirtual:调用对象的实例方法
  • invokeinterface:调用接口的方法
  • invokespecial:调用特殊处理的实例方法,如实例初始化方法、私有方法、父类方法
  • invokestatic:调用类方法
  • invokedynamic:运行时动态解析出调用点限定符所引用的方法
  • *return:方法返回指令

2.8 异常处理指令

  • athrow:Java代码中的throw语句。
  • 异常处理(catch语句)不是由字节码指令实现的,而是采用异常表实现的。

2.9 同步指令

  • 同步方法、同步代码块都是由管程实现的。
  • 同步方法不需要字节码指令控制,由方法表集合中的访问控制标志ACC_SYNCHRONIZED声明。
  • 同步代码块由monitorenter、monitorexit指令保证。

你可能感兴趣的:(类文件结构及字节码指令)