深入JVM:(七)Class文件结构

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构化约束,但任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为语言的产品交付媒介。

使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等其他语言的编译器一样可以把程序代码编译成Class文件,虚拟机并不关心Class的来源是何种语言。


image.png

Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比Java语言本身更加强大。因此,有一些Java语言本身无法有效支持的语言特性不代表字节码本身无法有效支持,这也为其他语言实现一些有别于Java的语言特性提供了基础。

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。

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

  • 无符号数属于基本的数据类型,以u1u2u4u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
  • 是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表
class文件格式.png

Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以在表中的数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。

1、魔数

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。

很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如gif或者jpeg等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。

Class文件的魔数为:0xCAFEBABE

2、Class文件版本号

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version)第7和第8个字节是主版本号(Major Version)

Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0-1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

3、常量池

常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一

常量池数据区.png

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的,常量池容量为十六进制数0x0016,即十进制的22,这就代表常量池中有21项常量,索引值范围为1~21。在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。Class文件结构中只有常量池的容量计数是从1开始。

常量池中主要存放两大类常量:字面量(Literal)符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

常量池中每一项常量都是一个表,在JDK 1.7之后共有14种结构各不相同的表结构数据。这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位

常量池项数据结构.png
常量池中数据类型.png

以上各种类型在常量池冲的标识和存储如下所示:

  1. int和float
    int和float.png
  2. long和double
    long和double.png
  3. UTF-8编码的字符串
    UTF-8编码的字符串.png
  4. String类型的字符串常量
    String类型的字符串常量.png

    CONSTANT_String_info结构体中的string_index的值指向了CONSTANT_Utf8_info结构体,而字符串的utf-8编码数据就在这个结构体之中。如下图所示:
    字符串.png

    我们可以看到CONSTANT_String_info结构体位于常量池的第#8个索引位置。而存放"Java虚拟机原理" 字符串的 UTF-8编码格式的字节数组被放到CONSTANT_Utf8_info结构体中,该结构体位于常量池的第#8个索引位置
  5. 类名
    VM会将某个Java 类中所有使用到了的类的完全限定名 以二进制形式的完全限定名 封装成CONSTANT_Class_info结构体中,然后将其放置到常量池里。CONSTANT_Class_info 的tag值为 7 。其结构如下:
    类.png
  6. Field字段
    CONSTANT_Fieldref_info.png

    CONSTANT_Name_Type_info.png

    字段描述符.png
  7. method方法
    CONSTANT_Methodref_info.png

4、访问标志、类索引、父类索引、接口索引集合

访问标志、类索引、父类索引、接口索引集合 在class文件中的位置如下图所示:
访问标志、类索引、父类索引、接口索引集合.png
  1. 访问标志(access_flags)
    访问标志(access_flags)紧接着常量池后,占有两个字节,总共16位,如下图所示:
    访问标志.png
  2. 类索引(this_class)
    一般情况下一个Java类源文件经过JVM编译会生成一个class文件,也有可能一个Java类源文件中定义了其他类或者内部类,这样编译出来的class文件就不止一个,但每一个class文件表示某一个类,至于这个class表示哪一个类,便可以通过 类索引 这个数据项来确定。
    类索引的作用,就是为了指出class文件所描述的这个类叫什么名字。 类索引紧接着访问标志的后面,占有两个字节,在这两个字节中存储的值是一个指向常量池的一个索引,该索引指向的是CONSTANT_Class_info常量池项
    类索引.png
  3. 父类索引(super_class)
    Java支持单继承模式,除了java.lang.Object 类除外,每一个类都会有且只有一个父类。class文件中紧接着类索引(this_class)之后的两个字节区域表示父类索引,跟类索引一样,父类索引这两个字节中的值指向了常量池中的某个常量池项CONSTANT_Class_info,表示该class表示的类是继承自哪一个类。
  4. 接口索引集合(interfaces)
    一个类可以不实现任何接口,也可以实现很多个接口,为了表示当前类实现的接口信息,class文件使用了如下结构体描述某个类的接口实现信息:
    接口索引集合.png

    由于类实现的接口数目不确定,所以接口索引集合的描述的前部分叫做接口计数器(interfaces_count),接口计数器占用两个字节,其中的值表示着这个类实现了多少个接口,紧跟着接口计数器的部分就是接口索引部分了,每一个接口索引占有两个字节

5、字段表集合

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

字段表集合的结构.png

字段表集合在class文件中的位置:
字段表集合在class文件中的位置.png

field_info结构体信息.png

FIeld_info的组成元素:
字段的作用域(public、private、protected修饰符)是实例变量还是类变量(static修饰符)可变性(final)并发可见性(volatile修饰符,是否强制从主内存读写)可否被序列化(transient修饰符)字段数据类型(基本类型、对象、数组)字段名称
class文件对数据类型的表示如下图所示:

class数据类型.png

如果使用final和static同时修饰一个field字段,并且这个字段是基本类型或者String类型的,那么编译器在编译这个字段的时候,会在对应的field_info结构体中增加一个ConstantValue类型的结构体,在赋值的时候使用这个ConstantValue进行赋值;如果该field字段并没有被final修饰,或者不是基本类型或者String类型,那么将在类构造方法()中赋值。

对于public static final init MAX=100; javac编译器在编译此field字段构建field_info结构体时,除了访问标志、名称索引、描述符索引外,会增加一个ConstantValue类型的属性表。


FIeld_info ConstantValue.png

6、方法表集合

方法表集合是指由若干个方法表(method_info)组成的集合。对于在类中定义的方法,经过JVM编译成class文件后,会将相应的method方法信息组织到一个叫做方法表集合的结构中,字段表集合是一个类数组结构,如下图所示:

方法表集合结构.png

method方法的描述-方法表集合在class文件中的位置如下所示:
方法表集合在class中的位置.png

method_info结构体的定义如下所示:
method_info结构体的定义.png

该结构体的定义跟描述field字段 的field_info结构体的结构几乎完全一致,如下图所示:
method_info.png

方法表的结构体由:访问标志(access_flags)名称索引(name_index)描述索引(descriptor_index)属性表(attribute_info)集合组成。

  • 访问标志(access_flags):
    method_info结构体最前面的两个字节表示的访问标志(access_flags),记录这这个方法的作用域、静态or非静态、可变性、是否可同步、是否本地方法、是否抽象等信息,

  • 名称索引(name_index):
    紧跟在访问标志(access_flags)后面的两个字节称为名称索引,这两个字节中的值指向了常量池中的某一个常量池项,这个方法的名称以UTF-8格式的字符串存储在这个常量池项中。如public void methodName(),很显然,“methodName”则表示着这个方法的名称,那么在常量池中会有一个CONSTANT_Utf8_info格式的常量池项,里面存储着“methodName”字符串,而mehodName()方法的方法表中的名称索引则指向了这个常量池项。

  • 描述索引(descriptor_index):
    描述索引表示的是这个方法的特征或者说是签名,一个方法会有若干个参数和返回值,而若干个参数的数据类型和返回值的数据类型构成了这个方法的描述,其基本格式为:(参数数据类型描述列表)返回值数据类型

    方法描述符.png

  • 属性表(attribute_info)集合:
    这个属性表集合非常重要,方法实现被JVM编译成JVM的机器码指令,机器码指令就存放在一个Code类型的属性表中;如果方法声明要抛出异常,那么异常信息会在一个Exceptions类型的属性表中予以展现

7、属性表集合

属性表(attribute_info)集合记录方法的机器指令和抛出异常等信息

属性表集合记录了某个方法的一些属性信息,这些信息包括:

  • 这个方法的代码实现,即方法的可执行的机器指令
  • 这个方法声明的要抛出的异常信息
  • 这个方法是否被@deprecated注解表示
  • 这个方法是否是编译器自动生成的

属性表(attribute_info)结构体的一般结构如下所示:

属性表(attribute_info).png

属性名称索引(attribute_name_index)占两个字节,其中的值是指向了常量池中的某一项,该项表示的字符串表用该attribute_info表示的是什么类型的属性表,如上所示如果指向的常量项是“code”,则表明该属性表表示的是代码实现,即可执行的机器码
属性长度(attribute_length)对于不同类型的属性表它们的属性长度和组织形式会有所不同。它的值表示紧跟其后的多少个字节是拿来表示这个属性信息的。
属性值(attribute_value)由若干个字节构成,字节的个数由属性长度中的值决定

Code类型的属性表--method方法中的机器指令的信息

Code类型的属性表(attribute_info)可以说是class文件中最为重要的部分,因为它包含的是JVM可以运行的机器码指令,JVM能够运行这个类,就是从这个属性中取出机器码的。除了要执行的机器码,它还包含了一些其他信息,如下所示:

Code类型的属性表.png

Code属性表的组成部分:

  • 机器指令(code)
    目前的JVM使用一个字节表示机器操作码,即对JVM底层而言,它能表示的机器操作码不多于2的 8 次方,即 256个

  • 属性长度(attribute_length)
    占有 4个字节,其内的值表示后面有多少个字节是属于此Code属性表的

  • 操作数栈深度的最大值(max_stack)
    占有 2 个字节,在方法执行的任意时刻,操作数栈都不应该超过这个值,虚拟机的运行的时候,会根据这个值来设置该方法对应的栈帧(Stack Frame)中的操作数栈的深度;

  • 最大局部变量数目(max_locals)
    占有 2个字节,其内的值表示局部变量表所需要的存储空间大小

  • 机器指令长度(code_length)
    占有 4 个字节,表示跟在其后的多少个字节表示的是机器指令

  • 显式异常表长度(exception_table_length)
    占有2个字节,如果在方法代码中出现了try{} catch()形式的结构,该值不会为空,紧跟其后会跟着若干个exception_table结构体,以表示异常捕获情况

  • 显式异常表(exception_table)
    占有8 个字节,start_pc,end_pc,handler_pc中的值都表示的是PC计数器中的指令地址。exception_table表示的意思是:如果字节码从第start_pc行到第end_pc行之间出现了catch_type所描述的异常类型,那么将跳转到handler_pc行继续处理。

  • 属性计数器(attribute_count)
    占有 2 个字节,表示Code属性表的其他属性的数目

  • 异常处理跳转信息(exception_table)
    如果代码中出现了try{}catch{}块,那么try{}块内的机器指令的地址范围记录下来,并且记录对应的catch{}块中的起始机器指令地址,当运行时在try块中有异常抛出的话,JVM会将catch{}块对应懂得其实机器指令地址传递给PC寄存器,从而实现指令跳转;

  • Java源码行号和机器指令的对应关系(LineNumberTable属性表)
    编译器在将java源码编译成class文件时,会将源码中的语句行号跟编译好的机器指令关联起来,这样的class文件加载到内存中并运行时,如果抛出异常,JVM可以根据这个对应关系,抛出异常信息,告诉我们我们的源码的多少行有问题,方便我们定位问题。这个信息不是运行时必不可少的信息,但是默认情况下,编译器会生成这一项信息,如果你项取消这一信息,你可以使用-g:none 或-g:lines来取消或者要求设置这一项信息。如果使用了-g:none来生成class文件,class文件中将不会有LineNumberTable属性表,造成的影响就是 将来如果代码报错,将无法定位错误信息报错的行,并且如果项调试代码,将不能在此类中打断点。

  • 局部变量表描述信息(LocalVariableTable属性表)
    局部变量表信息会记录栈帧局部变量表中的变量和java源码中定义的变量之间的关系,这个信息不是运行时必须的属性,默认情况下不会生成到class文件中。你可以根据javac指令的-g:none或者-g:vars选项来取消或者设置这一项信息。
    它有什么作用呢? 当我们使用IDE进行开发时,最喜欢的莫过于它们的代码提示功能了。如果在项目中引用到了第三方的jar包,而第三方的包中的class文件中有无LocalVariableTable属性表的区别如下所示:

    ide.png

下面用一个简单的类看一下方法表内部是怎样组织的:

public class Simple {
    public static synchronized final void greeting(){
        int a = 10;
    }
}

class文件结构如下所示:


Simple.class文件结构.png

Simple.class 中的() 方法

() 方法.png
  1. 方法访问标志(access_flags): 占有 2个字节,值为0x0001,即标志位的第 16 位为 1,所以该()方法的修饰符是:ACC_PUBLIC;
  2. 名称索引(name_index): 占有 2 个字节,值为 0x0004,指向常量池的第 4项,该项表示字符串“”,即该方法的名称是“”;
  3. 描述符索引(descriptor_index): 占有 2 个字节,值为0x0005,指向常量池的第 5 项,该项表示字符串“()V”,即表示该方法不带参数,并且无返回值(构造函数确实也没有返回值);
  4. 属性计数器(attribute_count): 占有 2 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;
  5. 属性表的名称索引(attribute_name_index):占有 2 个字节,值为0x0006,指向常量池中的第6 项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;
  6. 属性长度(attribute_length):占有4个字节,值为0x0000 0011,即十进制的 17,表明后续的 17 个字节可以表示这个Code属性表的属性信息;
  7. 操作数栈的最大深度(max_stack):占有2个字节,值为0x0001,表示栈帧中操作数栈的最大深度是1;
  8. 局部变量表的最大容量(max_variable):占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;
  9. 机器指令数目(code_length):占有4个字节,值为0x0000 0005,表示后续的5 个字节 0x2A 、0xB7、 0x00、0x01、0xB1表示机器指令;
  10. 机器指令集(code[code_length]):这里共有 5个字节,值为0x2A 、0xB7、 0x00、0x01、0xB1;
  11. 显式异常表集合(exception_table_count): 占有2 个字节,值为0x0000,表示方法中没有需要处理的异常信息;
  12. Code属性表的属性表集合(attribute_count): 占有2 个字节,值为0x0000,表示它没有其他的属性表集合,因为我们使用了-g:none 禁止编译器生成Code属性表的 LineNumberTable 和LocalVariableTable;

Simple.class 中的greeting() 方法

greeting() 方法.png
  1. 方法访问标志(access_flags): 占有 2个字节,值为 0x0039 ,即二进制的00000000 00111001,即标志位的第11、12、13、16位为1,根据上面讲的方法标志位的表示,可以得到该greeting()方法的修饰符有:ACC_SYNCHRONIZED、ACC_FINAL、ACC_STATIC、ACC_PUBLIC;
  2. 名称索引(name_index): 占有 2 个字节,值为 0x0007,指向常量池的第 7 项,该项表示字符串“greeting”,即该方法的名称是“greeting”;
  3. 描述符索引(descriptor_index): 占有 2 个字节,值为0x0005,指向常量池的第 5 项,该项表示字符串“()V”,即表示该方法不带参数,并且无返回值;
  4. 属性计数器(attribute_count): 占有 2 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;
  5. 属性表的名称索引(attribute_name_index):占有 2 个字节,值为0x0006,指向常量池中的第6 项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;
    6.属性长度(attribute_length):占有4个字节,值为0x0000 0010,即十进制的16,表明后续的16个字节可以表示这个Code属性表的属性信息;
  6. 操作数栈的最大深度(max_stack):占有2个字节,值为0x0001,表示栈帧中操作数栈的最大深度是1;
  7. 局部变量表的最大容量(max_variable):占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;
  8. 机器指令数目(code_length):占有4 个字节,值为0x0000 0004,表示后续的4个字节0x10、 0x0A、 0x3B、0xB1的是表示机器指令;
  9. 机器指令集(code[code_length]):这里共有4 个字节,值为0x10、 0x0A、 0x3B、0xB1 ;
  10. 显式异常表集合(exception_table_count): 占有2 个字节,值为0x0000,表示方法中没有需要处理的异常信息;
  11. Code属性表的属性表集合(attribute_count): 占有2 个字节,值为0x0000,表示它没有其他的属性表集合,因为我们使用了-g:none 禁止编译器生成Code属性表的 LineNumberTable 和LocalVariableTable;

Exceptions类型的属性表----method方法声明的要抛出的异常信息

看一下Exceptions类型的属性表(attribute_info)结构体是怎样组织的:


Exceptions类型的属性表.png

Exceptions类型的属性表(attribute_info)结构体由一下元素组成:

  • 属性名称索引(attribute_name_index):占有 2个字节,其中的值指向了常量池中的表示"Exceptions"字符串的常量池项;
  • 属性长度(attribute_length):它比较特殊,占有4个字节,它的值表示跟在其后面多少个字节表示异常信息;
  • 异常数量(number_of_exceptions):占有2 个字节,它的值表示方法声明抛出了多少个异常,即表示跟在其后有多少个异常名称索引;
  • 异常名称索引(exceptions_index_table):占有2个字节,它的值指向了常量池中的某一项,该项是一个CONSTANT_Class_info类型的项,表示这个异常的完全限定名称;

下面看一个简单的例子:

public interface Interface {
    public  void sayHello() throws Exception;
}
Interface.class文件结构.png
  1. 方法计数器(methods_count):中的值为0x0001,表明其后的方法表(method_info)就一个,即我们就定义了一个方法,其后会紧跟着一个方法表(method_info)结构体;
  2. 方法的访问标志(access_flags):的值是0x0401,二进制是00000100 00000001,第6位和第16位是1,对应上面的标志位信息,可以得出它的访问标志符有:ACC_ABSTRACT、ACC_PUBLIC。细心的读者可能会发现,在上面声明的sayHello()方法中并没有声明为abstract类型啊。确实如此,这是因为编译器对于接口内声明的方法自动加上ACC_ABSTRACT标志。
  3. 名称索引(name_index):中的值为0x0005,0x0005指向了常量池的第5项,第五项表示的字符串为“sayHello”,即表示的方法名称是sayHello
  4. 描述符索引(descriptor_index):中的值为0x0006,0x0006指向了常量池中的第6项,第6项表示的字符串为“()V” 表示这个方法的无入参,返回值为void类型
  5. 属性表计数器(attribute_count):中的值为0x0001,表示后面的属性表的个数就1个,后面紧跟着一个attribute_info结构体;
  6. 属性表(attribute_info):中的属性名称索引(attribute_name_index)中的值为0x0007,0x0007指向了常量池中的第7 项,第 7项指向字符串“Exceptions”,即表示该属性表表示的异常信息;
  7. 属性长度(attribute_length):中的值为:0x00000004,即后续的4个字节将会被解析成属性值;
  8. 异常数量(number_of_exceptions):中的值为0x0001,表示这个方法声明抛出的异常个数是1个;
  9. 异常名称索引(exception_index_table):中的值为0x0008,指向了常量池中的第8项,第8项表示的是CONSTANT_Class_info类型的常量池项,表示“java/lang/Exception”,即表示此方法抛出了java.lang.Exception异常。

参考:
《Java虚拟机原理图解》

你可能感兴趣的:(深入JVM:(七)Class文件结构)