JVM学习笔记 -- 类文件结构

时间:2021-05-25 14:03:51

  Java提出了"一次编写,到处运行"的口号,同一份程序可以在不同的平台上运行,实现语言无关性的基础是虚拟机和字节码存储格式,Java虚拟机不和包括Java在内的任何语言绑定。根据《深入理解Java虚拟机》中的内容,下面以《Java虚拟机规范(第2版)》,对应JDK1.4的Java虚拟机说明类文件的结构。

JVM学习笔记 -- 类文件结构

1、Class类文件结构

  Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列,中间没有任何分隔符。

1.1 数据结构和文件格式

  Class文件中只有2种数据结构:无符号数和表,无符号数是基本的数据类型,以u1、u2、u4、u8代表1个字节、2个字节、4个字节、8个字节的无符号数,而表是由无符号数和其他表组成的结构,整个Class文件本质上就是一张表。下面是Class文件中的数据项:

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count
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
attributes_info attributes attributes_count

  Class文件中字节长度、代表意义、先后顺序等细节都是严格规定不可改变的。下面具体介绍这些数据项。

1.2 魔数和Class文件版本

  Class文件前4个字节的魔数是用于身份识别,确定文件能否被虚拟机接受。Java Class文件的魔数是0xCAFEBABE。

  接下来的是Class文件的次版本号和主版本号。次版本号是小数点后面的部分,主版本号是小数点前面的部分。比如JDK1.2支持45.0~46.65535,如果有个Class文件的版本号是45.00,那么次版本号就是0,主版本号是45。

1.3 常量池

   constant_pool_count是Class文件常量池中常量的数量,如果这个数值是6,说明常量池中有5个常量,因为第0个是空出来用于表达"不引用任何一个常量池项目"的含义。常量池中主要放2类常量:字面量和符号引用。其中符号引用有下面3类:

1、类和接口的全限定名:把类的全名由"."改为"/"

2、字段的名称和描述符:

3、方法的名称和描述符:

  Class文件中字段和方法的符号引用需要经过运行期转换后才能得到真正的内存入口地址,当虚拟机运行时,从常量池获取对应的符号引用,然后在类创建或运行时解析、翻译到具体的内存地址。JDK1.7中一共有下面14种常量:

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_MethodHandler_info 15 表示方法句柄
CONSTANT_MethodType_info 16 表示方法类型
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

  这14种常量的表的第一位都是一个u1类型的标志位,用于表示常量类型。引用书中的Java代码进行分析:

package or.fenixsoft.clazz;

public class TestClass {
    private int m;
    public int inc(){
        return m + 1;
    }
}

  对应解析出来的Class文件字节码:

......
Constant pool:
const #1 = class                #2;  //org/fenixsoft/clazz/TestClass
const #2 = Asciz                org/fenixsoft/clazz/TestClass;
const #3 = class                #4;  //java/lang/Object
const #4 = Asciz                java/lang/Object
const #5 = Asciz                m;
const #6 = Asciz                I;
const #7 = Asciz                <init>;
const #8 = Asciz                ()V;
const #9 = Asciz                Code;
const #10 = Method              #3.#11;  //java/lang/Object."<init>":()V
const #11 = NameAndType         #7:#8;  //"<init>":()V
const #12 = Asciz               LineNumberTable;
const #13 = Asciz               LocalVariableTable
const #14 = Asciz               this;
const #15 = Asciz               Lorg/fenixsoft/clazz/TestClass;;
const #16 = Asciz               inc;
const #17 = Asciz               ()I;
const #18 = Field               #1.#19;  //org/fenixsoft/clazz/Testclass.m:I
const #19 = NameAndType         #5:#6;  //m:I
const #20 = Asciz               SourceFile;
const #21 = Asciz               TestClass.java;

   下面是对应的常量池结构:

1 CA FE BA BE 00 00 00 32
2 00 16 07 00 02 01 00 1D
3 6F 72 67 2F 66 65 6E 69
4 78 73 6F 66 74 2F 63 6C
5 ......

  第二行的07开始就是常量池项目,它的常量结构如下表:

类型 名称 数量
u1 tag 1
u2 name_index 1

  7对应的是CONSTANT_Class_info,接下来的0002表示值为第二个常量。第二个常量是01,即UTF-8编码的字符串,它的结构为:

类型 名称 数量
u1 tag 1
u2 length 1
u1 bytes length

  001D即是29,接下来29个字节组成的字符串即是第二个变量代表的字符串。CONSTANT_Utf8_info的长度类型是u2,代表字段名、方法名不能超过65535,正常也不会取这么长的名字吧。其他常量格式不给出,都是按照这样的方式存储的。

1.4 访问标志

  访问标志用于标识类或接口层次的访问信息,16个标志位定义了8个,没有使用到的一致为0:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否声明为final
ACC_SUPER 0x0020 是否允许使用invokespecial指令新语义
ACC_INTERFACE 0x0200 是否是接口
ACC_ABSTRACT 0x0400 是否是abstract类型
ACC_SYNTHETIC 0x1000 标识类非由用户代码产生
ACC_ANNOTATION 0x2000 是否是注解
ACC_ENUM 0x4000 是否是枚举

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

  this_class是类的全限定名,super_class是父类的全限定名,接口集合则是根据implements后面的接口顺序从左到右排序。这几项都指向CONSTANT_Class_info常量。

1.6 字段表集合

  字段表用于描述类或接口的变量,方法内部定义的局部变量不包含在内。下面是fields字段表的结构:

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

1.6.1 access_flags

  字段修饰符放在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

1.6.2 name_index

  这个表示字段的简单名称,简单名称是指没有类型和参数修饰的方法或字段名,比如常用的有个toString的简单名称。

1.6.3 descriptor_index

  描述符,用于描述字段的数据类型、方法的参数列表和返回值。基本数据类型和代表无返回值的void类型用一个大写字符表示,而对象类型则用字符L加对象的全限定名来表示,如下表:

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

  对于数组类型,每一个维度用"["表示,如定义一个"java.lang/String[][]"类型,则是"[[Ljava/lang/String",一个整型数组"int[]",则是"[I"。

1.6.4 attributes

  属性表,用来存储一些额外的信息,如果有字段"final static int m = 123",则会存在一个名称为ConstantValue的属性,指向常量123。

1.7 方发表集合

  方法表的结构和字段表一致,依次包括access_flags、name_index、descriptor_index、attributes_count、attributes。方法表集合可能会有编译器自动添加的方法,比如类构造器"<clinit>"方法和实例构造器"<init>"方法。

1、类构造器:编译器自动收集类中所有类变量和静态语句块中的语句合并而成,顺序为源代码顺序。

2、实例构造器:编译器自动收集类中所有实例变量和非静态语句块中的语句合并而成。

1.7.1 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_BRODGE 0x0040 方法是否为由编译器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为sbstract
ACC_STRICTFP 0x0800 方法是否为strictfp
ACC_SYNTHETIC 0x1000 方法是否为编译器自动产生的

1.7.2 descriptor_index

  描述符先按照参数列表,后返回值的顺序描述,参数列表按照参数顺序方法小括号"()"中,方法void inc(int a,char b)的描述符为"(IC)V",java.lang.String.toString()的,描述符为"()Ljava/lang/String"

1.7.3 attributes

  方法里的Java代码会存放在属性表中,在一个"Code"的属性中。

1.8 属性表集合

  在Class文件、字段名和方法表中都可以携带自己的属性表集合,用于描述某些场景专有的信息。

1.8.1 Code属性

  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_length code_length
u2 exception_table_length 1
exception_info exception_table exception_table_length
u2 attributes_count 1
attribute_info attributes attributes_count

1、attribute_name_index

  这个属性项是一个指向CONSTANT_Utf8_info常量的索引,值为"Code",代表属性名称。

2、attribute_length

  属性值的长度,等于整个属性表的长度减去6个字节(属性名和属性长度占据的6个字节)。

3、max_stack

  代表错作数栈深度的最大值,虚拟机运行时根据这个值分配栈帧中的操作栈深度。

4、max_locals

  局部变量表所需的存储空间。单位是Slot。长度不超过32位的数据类型,每个局部变量占用一个Slot,double和long占用2个Slot。局部变量占据Slot的和不等于max_locals,代码执行超出一个局部变量的作用域时,其占据的Slot可以复用。

5、code

  用于存储源代码编译生成的字节码指令,指令是u1类型的单字节。有些指令是带有参数的,虚拟机知道如何理解。另外code_length虽然定义 了u4类型的长度,实际虚拟机规范限制了一个方法的字节码不能超过65535条。

6、exception_table

  方法的异常处理表,它的结构为:

类型 名称 数量
u2 start_pc 1
u2 end_pc 1
u2 handler_pc 1
u2 catch_type 1

  含义为:如果字节码在第start_pc行到end_pc(不包括end_pc)出现了类型为catch_type或其子类的异常,则转到第handler_pc行继续处理。当catch_type的值为0是,任何异常都要转到handler_pc处理。

1.8.2 Exception属性

  这个属性跟Code属性平级,与上面的异常表不同,是方法描述时在throws关键字后面列举的异常。结构为:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_exception 1
u2 exception_index_table number_of_exception

  exception_index_table表示抛出的受检异常,是一个指向常量池CONSTANT_Class_info型常量的索引。