类文件结构

来源:互联网 发布:mac 软件安装桌面 编辑:程序博客网 时间:2024/06/15 15:42

Class类文件的结构

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。


Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前[1]的方式分割成若干个8位字节进行存储。


根据Java虚拟机规范的规定,Class文件格式采用伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础。


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


表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,它由表6-1所示的数据项构成。


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


魔数与Class文件的版本

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如gif或者jpeg等在文件头中都存有魔数。


紧接着魔数的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文件。


package org.clazz;public class TestClass{ private int m;public int inc(){return m+1;}}


开头4个字节的十六进制表示是0xCAFEBABE,

代表次版本号的第5个和第6个字节值为0x0000,

而主版本号的值为0x0033,也即是十进制的51,该版本号说明这个文件是可以被JDK 1.7或以上版本虚拟机执行的Class文件。


从JDK 1.1到JDK 1.7,主流JDK版本编译器输出的默认和可支持的Class文件版本号


这种顺序称为“Big-Endian”,具体是指最高位字节在地址最低位、最低位字节在地址最高位的顺序来存储数据,它是SPARC、PowerPC等处理器的默认多字节存储顺序,而x86等处理器则是使用了相反的“Little-Endian”顺序来存储数据。


常量池

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。


由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count),计数是从1开始的,如果十六进制数0x0013,即十进制的19,这就代表常量池中有19项常量,索引值范围为1~19。


在Class文件格式规范制定之时,设计者将第0项常量空出来,某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。


Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都是从0开始的。


常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。

字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。

而符号引用则属于编译原理方面的概念,包括了下面三类常量:

类和接口的全限定名(Fully Qualified Name)

字段的名称和描述符(Descriptor) 

方法的名称和描述符


Java代码在进行Javac编译的时候,是在虚拟机加载Class文件的时候进行动态连接。

在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。


常量池中每一项常量都是一个表,在JDK 1.7之前共有11种结构各不相同的表结构数据,在JDK 1.7中为了更好地支持动态语言调用,又增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。


这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量属于哪种常量类型。



之所以说常量池是最烦琐的数据,是因为这14种常量类型各自均有自己的结构。上图常量池的第一项常量,它的标志位是0x0A,即十进制的10,查表 这个常量属于CONSTANT_Methodref_info类型




在JDK的bin目录中,Oracle公司已经为我们准备好一个专门用于分析Class文件字节码的工具:javap

F:\>javap -v TestClass.classClassfile /F:/TestClass.class  Last modified 2017-8-15; size 285 bytes  MD5 checksum cbd7dbd2cc55984adc03a6383debcf45  Compiled from "TestClass.java"public class org.clazz.TestClass  SourceFile: "TestClass.java"  minor version: 0  major version: 51  flags: ACC_PUBLIC, ACC_SUPERConstant pool:   #1 = Methodref          #4.#15         //  java/lang/Object."<init>":()V   #2 = Fieldref           #3.#16         //  org/clazz/TestClass.m:I   #3 = Class              #17            //  org/clazz/TestClass   #4 = Class              #18            //  java/lang/Object   #5 = Utf8               m   #6 = Utf8               I   #7 = Utf8               <init>   #8 = Utf8               ()V   #9 = Utf8               Code  #10 = Utf8               LineNumberTable  #11 = Utf8               inc  #12 = Utf8               ()I  #13 = Utf8               SourceFile  #14 = Utf8               TestClass.java  #15 = NameAndType        #7:#8          //  "<init>":()V  #16 = NameAndType        #5:#6          //  m:I  #17 = Utf8               org/clazz/TestClass  #18 = Utf8               java/lang/Object{  public org.clazz.TestClass();    flags: ACC_PUBLIC    Code:      stack=1, locals=1, args_size=1         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."<init>":()V         4: return      LineNumberTable:        line 2: 0  public int inc();    flags: ACC_PUBLIC    Code:      stack=2, locals=1, args_size=1         0: aload_0         1: getfield      #2                  // Field m:I         4: iconst_1         5: iadd         6: ireturn      LineNumberTable:        line 5: 0}




访问标志

在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。


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

排列在访问标志之后,类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。



接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。
接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。


字段修饰符放在access_flags项目中,是一个u2的数据类型,

跟随access_flags标志的是两项索引值:name_index和descriptor_index。
它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符

对于数组类型,每一维度将使用一个前置的“[”字符来描述。 用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。

字段表都包含的固定数据项目到descriptor_index为止就结束了,不过在descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。

字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。

方法表集合

访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)



方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面

如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”方法。

属性表集合




对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。

1.Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。

attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表了该属性的属性名称,attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6个字节。

max_stack代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。

max_locals代表了局部变量表所需的存储空间。max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。方法参数(包括实例方法中的隐藏参数“this”)、显式异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。

code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理解。我们知道一个u1数据类型的取值范围为0x00~0xFF,对应十进制的0~255,也就是一共可以表达256条指令。

Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。


显式异常处理表
如果当字节码在第start_pc行到第end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转向到handler_pc处进行处理。

package org.clazz;public class TestClass {    private int m;    public int inc() {        int x;        try {            x = 1;            return x;        } catch (Exception e) {            x = 2;            return x;        } finally {            x = 3;        }    }}
F:\>javap -v TestClass.classClassfile /F:/TestClass.class  Last modified 2017-8-16; size 433 bytes  MD5 checksum 0a7171d89c29166ea628992e27f96d84  Compiled from "TestClass.java"public class org.clazz.TestClass  SourceFile: "TestClass.java"  minor version: 0  major version: 51  flags: ACC_PUBLIC, ACC_SUPERConstant pool:   #1 = Methodref          #4.#18         //  java/lang/Object."<init>":()V   #2 = Class              #19            //  java/lang/Exception   #3 = Class              #20            //  org/clazz/TestClass   #4 = Class              #21            //  java/lang/Object   #5 = Utf8               m   #6 = Utf8               I   #7 = Utf8               <init>   #8 = Utf8               ()V   #9 = Utf8               Code  #10 = Utf8               LineNumberTable  #11 = Utf8               inc  #12 = Utf8               ()I  #13 = Utf8               StackMapTable  #14 = Class              #19            //  java/lang/Exception  #15 = Class              #22            //  java/lang/Throwable  #16 = Utf8               SourceFile  #17 = Utf8               TestClass.java  #18 = NameAndType        #7:#8          //  "<init>":()V  #19 = Utf8               java/lang/Exception  #20 = Utf8               org/clazz/TestClass  #21 = Utf8               java/lang/Object  #22 = Utf8               java/lang/Throwable{  public org.clazz.TestClass();    flags: ACC_PUBLIC    Code:      stack=1, locals=1, args_size=1         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."<i":()V         4: return      LineNumberTable:        line 2: 0  public int inc();    flags: ACC_PUBLIC    Code:      stack=1, locals=5, args_size=1         0: iconst_1//try块中的x=1         1: istore_1         2: iload_1//保存x到returnValue中,此时x=1         3: istore_2         4: iconst_3//finaly块中的x=3         5: istore_1         6: iload_2//将returnValue中的值放到栈顶,准备给ireturn返回         7: ireturn         8: astore_2//给catch中定义的Exception e赋值,存储在Slot 2中         9: iconst_2//catch块中的x=2        10: istore_1        11: iload_1//保存x到returnValue中,此时x=2        12: istore_3        13: iconst_3//finaly块中的x=3        14: istore_1        15: iload_3//将returnValue中的值放到栈顶,准备给ireturn返回        16: ireturn        17: astore        4//如果出现了不属于java.lang.Exception及其子类的异常才会走到这里        19: iconst_3//finaly块中的x=3        20: istore_1        21: aload         4//将异常放置到栈顶,并抛出        23: athrow      Exception table:         from    to  target type             0     4     8   Class java/lang/Exception             0     4    17   any             8    13    17   any            17    19    17   any      LineNumberTable:        line 8: 0        line 9: 2        line 14: 4        line 10: 8        line 11: 9        line 12: 11        line 14: 13      StackMapTable: number_of_entries = 2           frame_type = 72 /* same_locals_1_stack_item */          stack = [ class java/lang/Exception ]           frame_type = 72 /* same_locals_1_stack_item */          stack = [ class java/lang/Throwable ]}
如果没有出现异常,返回值是1;
如果出现了Exception异常,返回值是2;
如果出现了Exception以外的异常,方法非正常退出,没有返回值。

字节码中第0~4行所做的操作就是将整数1赋值给变量x,并且将此时x的值复制一份副本到最后一个本地变量表的Slot中(这个Slot里面的值在ireturn指令执行前将会被重新读到操作栈顶,作为方法返回值使用。returnValue)。

如果这时没有出现异常,则会继续走到第5~9行,将变量x赋值为3,然后将之前保存在returnValue中的整数1读入到操作栈顶,最后ireturn指令会以int形式返回操作栈顶中的值,方法结束。

如果出现了异常,PC寄存器指针转到第10行,第10~20行所做的事情是将2赋值给变量x,然后将变量x此时的值赋给returnValue,最后再将变量x的值改为3。方法返回前同样将returnValue中保留的整数2读到了操作栈顶。从第21行开始的代码,作用是变量x的值赋为3,并将栈顶的异常抛出,方法结束。

2.Exceptions属性

在方法表中与Code属性平级的一项属性,Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常。

方法可能抛出number_of_exceptions种受查异常,每一种受查异常使用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。

3.LineNumberTable属性

描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none 或-g:lines 选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。

line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。


4.LocalVariableTable属性

描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none 或-g:vars 选项来取消或要求生成这项信息。如果没有生成这项属性,当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。


local_variable_info项目代表了一个栈帧与源码中的局部变量的关联

start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。
name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。
index是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时(double和long),它占用的Slot为index和index+1两个。

SourceFile属性

用于记录生成这个Class文件的源码文件名称。可以分别使用Javac的 -g:none或 -g:source选项来关闭或要求生成这项信息。在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。这个属性是一个定长的属性.

sourcefile_index数据项是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名。

ConstantValue属性

通知虚拟机自动为静态变量赋值。只有类变量才可以使用这项属性。对于实例变量 的赋值是在实例构造器<init>方法中进行的;对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性。目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量,并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化。

虽然有final关键字才更符合“ConstantValue”的语义,但虚拟机规范中并没有强制要求字段必须设置了ACC_FINAL标志,只要求了有ConstantValue属性的字段必须设置ACC_STATIC标志而已,对final关键字的要求是Javac编译器自己加入的限制。由于Class文件格式的常量类型中只有与基本属性和字符串相对应的字面量。

从数据结构中可以看出,ConstantValue属性是一个定长属性,它的attribute_length数据项值必须固定为2。constantvalue_index数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一种。

InnerClasses属性

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

数据项number_of_classes代表需要记录多少个内部类信息,每一个内部类的信息都由一个inner_classes_info表进行描述。

inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_Class_info型常量的索引,分别代表了内部类和宿主类的符号引用。
inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称,如果是匿名内部类,那么这项值为0。
inner_class_access_flags是内部类的访问标志,类似于类的access_flags,


Deprecated及Synthetic属性

都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。
Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@deprecated注释进行设置。
Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的,在JDK 1.5之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们访问标志中的ACC_SYNTHETIC标志位,其中最典型的例子就是Bridge Method。所有由非用户代码产生的类、方法及字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器“<init>”方法和类构造器“<clinit>”方法。


StackMapTable属性

在JDK 1.6发布后增加到了Class文件规范中,它是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

Signature属性

可选的定长属性,可以出现于类、属性表和方法表结构的属性表中。在JDK 1.5中大幅增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。因为Java语言的泛型采用的是擦除法实现的伪泛型,在字节码(Code属性)中,泛型信息编译(类型变量、参数化类型)之后都通通被擦除掉。使用擦除法的好处是实现简单(主要修改Javac编译器,虚拟机内部只做了很少的改动)、非常容易实现Backport,运行期也能够节省一些类型所占的内存空间。但坏处是运行期就无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得到泛型信息。Signature属性就是为了弥补这个缺陷而增设的,现在Java的反射API能够获取泛型类型,最终的数据来源也就是这个属性。



BootstrapMethods属性





原创粉丝点击