天天看点

Java虚拟机——初探字节码class文件内部结构

之前介绍过Java编译器如何将Java源码编译成字节码class文件。

Java虚拟机——从Java源码到字节码到底经历了什么

那么最终的到的字节码文件是怎样的一个文件,内部结构又是如何?此文对字节码class文件的内部结构进行初步探索,介绍其各个重要组成部分,对之后的Java虚拟机学习做好基础。

下面展示了一个class文件的构成,其中u2、u4等表示类型,分别表示占2、4个字节的数据,属于class文件的基本类型。cp_info表示常量池类型,field表示成员变量类型,method表示类或接口的方法类型,attribute表示属性类型。

ClassFile {
    u4 magic;
    u2 minor_version;
    u2 major_version;
    u2 constant_pool_count;
    cp_info constant_pool[constant_pool_count - 1];
    u2 access_flags;
    u2 this_class;
    u2 super_class;
    u2 interface_count;
    u2 interfaces[interface_count];
    u2 fields_count;
    field info_fields[fields_count];
    u2 methods_count;
    method info_methods[methods_count];
    u2 attributes_count;
    attribute info_attributes[attributes_count];
}           

1. magic

每个字节码文件开头都包含4个字节的magic number:0xCAFEBABE 用来快速校验是否是Java Class文件。Cafe babe和Java一样,命名的由来都和咖啡有关。

2. minor_version 和 major_version

这4个字节先后包含的是次版本号和主版本号。JVM在校验完魔法数后紧接着就会检查版本号是否在JVM支持的有效范围,如果过高版本的字节码文件在低版本的JVM上是无法执行的。

3. constant_pool_count 和 constant_pool[]

常量池,里面存储了符号引用和字表量,包含了类和接口名、字段名和描述符、方法名和描述符、字符串、final变量值等等,这些信息以列表的形式存储在常量池列表中。constant_pool_count即常量池计数项,用于记录常量池中的常量数量,计数的总和等于1+常量池列表的项数,这里的1代表常量池表项,索引为0。

值得一提的是,常量池是字节码文件内部结构中与外部关联最多的数据项,也是占空间最大的项。但是class文件不保存各个方法字段的内存布局的信息,内存的分配是在运行时完成的。当类加载器将类加载到内存时,常量池中指向类和非final静态字段的符号引用会转换成直接引用指向指定的内存地址。当初始化生成一个对象时,常量池中指向非静态字段的符号引用会转换成直接引用指向指定的内存地址。当执行某个方法时,方法对应的当前栈帧中的动态链接会将常量池中指向方法的符号引用转换为调用方法的直接引用。

4. access_flags

访问标志项用来描述这个文件类型的一些访问标志信息。区分这个文件定义的是类还是接口,类或接口包含了哪些修饰符,是抽象的还是公共的或是final的。

Java类的所有access flags标志

类型 作用
ACC_PUBLIC 0x0001 声明为public,可以从它的包外访问
ACC_FINAL 0x0010 声明为final,不允许有子类
ACC_SUPER 0x0020 用invokespecial指令处理超类的调用
ACC_INTERFACE 0x0200 表明是一个接口
ACC_ABSTRACT 0x0400 声明为abstract,不能被实例化

5. this_class 和 super_class

this_class的值指向了常量池中类型为CONSTANT_Class_info的一个类的常量,进一步可以查到this_class的全限定名,super_class同理。因为Java只支持单继承,因此父类只有一个,super_class的值为0时说明这个class文件的类直接继承了java.lang.Object,其他情况不允许为0。

6. interface_count 和 interfaces[]

interface_count用来记录interfaces的容量,u2类型的interfaces代表这个类实现的接口以及接口的父接口的常量池索引集合,指向常量池接口常量。如果这个类没有实现任何一个接口interface_count的值为0。

7. fields_count 和 info_fields[]

fields_count用来记录类或接口中声明的变量的个数,info_fields是field类型列表,用来描述类或接口中声明的变量,以上的变量指的都是指成员变量(类变量和实例变量),不包括方法内部的局部变量(方法内部的局部变量在方法在执行的时候存储在当前栈帧中的局部变量表中,也就是我们说的局部变量存储在Java栈区)。

field类型的结构

类型 名称 说明
u2 access_flags 声明成员变量时使用的访问标志
u2 name_index 成员变量的名称索引
u2 descriptor_index 变成员量的描述符索引
u2 attributes_count 属性列表中属性的个数
attribute_info attributes 成员变量的属性

成员变量访问标志包含:

ACC_PUBLIC, ACC_PRIVATE, ACC_PROTECTED, ACC_STATIC,

ACC_FINAL, ACC_VOLATILE(并发可见性)

name_index和descriptor_index指向常量池中成员变量的名称和描述符。成员变量的名称并不是全限定名,而是没有类型的简单名称。成员变量的描述符描述其类型,描述符的格式:基本数据类型以及void类型都用一个特定的大写字符表示,而对象类型用字符L加对象的全限定名来表示,而数组类型,N维数组就在前面个N个[,例如:String[][]的描述符就是“[[Ljava/lang/String”。

attributes_count记录成员变量的属性个数,如果没有则为0。在这里会出现的属性有ConstantValue属性,后面会介绍这个属性。

需要说明info_fields中不包含从父类或父接口中继承来的字段信息。

8. methods_count 和 info_methods[]

methods_count用来记录这个类中的所有方法个数,info_methods是method类型数组,用来描述类或接口中声明的方法。method类型的结构类似field。

method类型的结构

类型 名称 说明
u2 access_flags 声明方法时使用的访问标志
u2 name_index 方法的名称索引
u2 descriptor_index 方法的描述符索引
u2 attributes_count 属性列表中属性的个数
attribute_info attributes 方法的属性

类或接口中方法的访问标志包含:

ACC_PUBLIC, ACC_PRIVATE, ACC_PROTECTED,

ACC_ABSTRACT,

ACC_STATIC, ACC_FINAL, ACC_SYNCHRONIZED, ACC_NATIVE, ACC_STRICT

name_index和descriptor_index指向常量池中方法名称和描述符。方法的名称并不是全限定名,而是简单名称。方法的描述符描述了方法的参数类型列表和返回值类型,参数类型列表的排序必须严格按照顺序。方法描述符的格式是“(参数1类型,参数2类型...)返回类型”例如,方法java.lang.String toString()的描述符为“()Ljava/lang/String”(L是表示引用类型),方法 void init()方法的描述符为“()V”。这里要说明的是,在Java语法中是不允许存在除返回类型不同其他完全相同的两个方法,这会导致编译不通过,但是Class文件允许这样的情况存在。

讲到这里,方法的访问标志、方法名称以及方法的描述符(参数列表和返回类型)都各归其位,那么最主要的方法体中的代码呢?好了,方法属性隆重登场,方法体中的方法代码经过编译器编译成一条条指令存放在方法的属性类型中有个“Code”属性中。其实在栈帧中,局部变量表和操作数栈所需的容量大小在编译期就可以完全被确定下来,并保存在方法的Code属性中。不要忘记,方法还能抛出异常,而对于可能抛出的异常信息就存储在属性中的Exception属性中。

同feild一个methods列表中不包含父类或父接口中继承来的方法。

9. attributes_count 和 info_attributes[]

上文在field和method中已经出现并提到过attribute属性,这里的info_attributes是在最外层的class文件中,例如,一些内部类和匿名类的信息就存储在对应的属性项中。下面会列举一些Java预定义的属性。先来看attribute的内部结构。

attribute类型结构

类型 名称 说明
u2 attribute_name_index 常量池中属性名称的索引
u4 attribute_lenght 属性数据的长度(以字节计算)
u1 info 包含的属性数据,长度为attribute_lenght

Java虚拟机规范规定,只要遵循一定规则,任何人都能向class文件中加入属性,这里不做深入探讨。下面列举一些Java预定义的属性。

属性名称 说明 长度
Code Java代码编译成的字节码指令 可变
ConstantValue 只有同时被final和static修饰的成员变量才有ConstantValue属性,且限于基本类型和String 固定
Deprecated 过时的类、方法或成员变量 固定
Exceptions 方法可能抛出的异常 可变
InnerClasses 内部类列表 可变
SourceFile 源文件名称 固定
Synthetic 标识类,方法或成员变量是否是编译器自动生成的 固定
RuntimeVisibleAnnotations 运行时可见注解 可变
RuntimeInvisibleAnnotations 运行时不可见注解 可变
BootstrapMethods 用于保存invokedynamic指令引用的引导方法限定符 可变
LineNumberTable 源码行数与字节码指令对应关系 可变
LocalVariableTable 方法的局部变量描述 可变
StackMapTable 检查和处理目标方法的局部变量和操作数栈所需的类型是否匹配 可变

以上是列举的部分属性,和class文件中的其他项不同的是,属性列表没有严格的顺序要求,属性项的长度也可以是可变的,因此需要数据结构中的attribute_lenght以字节为单位记录属性数据的总长度。下面列举下几个属性的内部结构。

Code属性结构

类型 名称 说明
u2 attribute_name_index 常量池中属性名称的索引
u4 attribute_length 属性数据的长度(以字节计算)
u2 max_stack 方法的操作数栈的最大长度(以字节计算)
u2 max_locals 局部变量所需存储的空间长度(以Slot计算)
u4 code_length 方法的字节码流长度(以字节计算)
u1 code 方法的字节码指令的一系列字节流
u2 exception_table_length exception_table的中异常的个数
exception_info exception_table 异常表
u2 attributes_count Code的属性个数
attribute_info attributes Code属性(LineNumberTable, LoaclVaribaleTable和StackMapTable)

ConstantValue属性结构

类型 名称 说明
u2 attribute_name_index 常量池中属性名称的索引
u4 attribute_length 属性数据的长度(固定为2个字节)
u2 constantvalue_index 常量池中常量值的索引

Exception属性

类型 名称 说明
u2 attribute_name_index 常量池中属性名称的索引
u4 attribute_length 属性数据的长度(以字节计算)
u2 number_of_exceptions throws关键字后面列举的异常数量
u2 exception_index_table 常量池中throws关键字后面列举的异常的索引

总结

到这里字节码class文件大致的内部结构就介绍完了。

Java不像C或者C++那样编译完成后就保存了类、方法和变量的内存布局信息,字节码文件只有符号引用,而没有直接指向内存空间的引用,是属于静态文件,因此才会有之后的Java虚拟机加载字节码文件进行动态连接,通过解析翻译将符号引用转换成直接引用,这也是Java的魅力所在。

对于Java虚拟机的初学者来说,刚接触这些内容可能感觉会比较枯燥,没有太多感知,容易忘记。但是随着之后的学习,当了解了类加载的过程,方法的执行和栈帧的结构,堆内存以及对象的初始化等一系列原理后,再回过头看这些内容,我相信肯定会有和第一次学习时有不一样的感受。了解字节码文件内部结构是接下去学习JVM的基石,当掌握JVM这套编译执行流程后,字节码文件的结构也自然会熟记在心。

继续阅读