欢迎大家来到IT世界,在知识的湖畔探索吧!
Class类文件的结构
我们知道JVM最终是通过加载.class文件到内存后进行编译或解释执行的,那么这个字节码文件中到底是什么呢?本次我们一起探究一下。
Class文件是一组以8位为基础单位的二进制流,当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储。包含两种数据类型:无符号数和表。
无符号数:属于基本的数据类型,以u1 u2 u4 u8 分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用,数量值或者按照UTF-8编码构成字符串值。
表:由多个无符号数或其他表作为数据项构成的复合数据类型,所有表习惯性以“_info”结尾。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干连续的数据项的形式。
魔数与Class文件版本
每个Class文件的前4个字节为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。
Class文件的魔术的值为0xCAFEBABE,紧接着4个字节存储的是Class文件的版本号,第5和第6个字节是次版本号(Minor Version),第7和第8个是主版本号(Major Version)。
Java的版本号从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1(JDK1.0~1.1使用了45.0~45.3的版本号)。
高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
常量池
紧接着是常量池入口。常量是Class文件空间最大的数据项目之一。常量池容量为u2数据类型,其索引值是从1开始的,例如0x0016为十进制22,但实际只有21(1~21)个常量。常量池中索引为0代表不引用任何常量。Class文件中只有常量池索引是从1开始的,其他的都与一般习惯相同,从0开始。
常量池主要存放两类常量:字面常量和符号引用。字面常量如文本字符串、被声明为final的常量值。而符号引用常量包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
Class文件加载的过程中虚拟机需要根据索引去常量池解析对应的值。
常量池中每一项常量都是一个表,共有11中各不相同的表结构数据,这些表结构都有一个特点就是第一位是一个u1类型的标志位,代表常量类型。
CONSTANT_Class_info的结构主要包括 u1(表示常量类型) u2(name_index) name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型的常量,此常量代表了这个类或者接口的全限定名。
CONSTANT_Utf8_info的结构为u1(tag) u2(length) u1(bytes)(length 个), 其中length字段说明这个utf-8编码的字符串的长度是多少字节,它后面紧跟着的长度为length字节的连续数据。utf-8编码中从’\u0001’到’\u007f’之间的字符的缩略吗使用一个字节表示,从’\u0080’到’\u07ff’之间的所有字符的缩略编码用两个字节表示。从’\u0800’到’\uffff’之间的所有字符缩略码使用三个字节表示。我们使用javap -verbose 可以将字节码内容打印出来。
常量池中数据类型的结构如下:
常量池后面是u2代表访问标志(access_flags),这个标志用于标识当前class的一些类或者接口层次的访问信息。具体如下:
this_class 和 super_class都是一个u2索引类型,用于标识类和父类的全限定名。interfaces用于标识接口集合
类中字段
field_info 用于描述接口或类中声明的变量。包括静态和非静态的变量。字段表的结构如下:
其中name_index描述字段名称 descriptor描述的是字段的描述符(这个类似jni中的描述类似)attribute_info是字段的额外描述信息。
filed_info中不会列出继承来的字段,但有可能会列出原本java代码中不存在的字段,譬如内部类会添加指向外部类的字段。
方法表
方法表的内容和字段表基本是一致的,依次包括 access_flags name_index decriptor_index attributes.
方法里的java代码,经过编译成字节码指令后,存放在方法属性表集合中一个名为code的属性里面。在java语言中只靠返回值类型不同不能构成方法的重载。而在class文件中是可以的。
属性表(attribute_info)集合
与class文件中其他的数据项要求严格的顺序、长度和内容不同,属性表并不要求各个属性有严格的顺序,并且只要不与已知的属性名重复就可以。任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。为了能正确地解析class文件,虚拟机应当识别的属性如下:
对于每个属性它的名称它的名称为u2的索引类型,而属性值的结构则是完全自定义的,只需要声明属性值所占用的位长度即可。其主要字段包括:u2(attribute_name_index) u2(attribute_length) u1(info)
Code属性
并非所有的方法都必须有这个属性,例如接口或抽象类中的方法就不存在Code属性。Code属性的结构如下:
attribute_length 指示了属性值的长度,由于属性名称索引和属性长度移动是6个字节,所以属性值的长度固定为整个属性表的长度减去6个字节。
max_stack代表了操作数栈深度的最大值。在执行任意指令的时候操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈中操作栈的深度。
max_locals代表了局部变量表所需要的存储空间。在这里max_locals单位是slot,slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte,char,float,int,short,bootlean,reference,returnAddress等长度不超过32位的数据类型,每个局部变量占用1个slot,而double和long这两种64位的数据类型则需要2个slot.方法参数、显式异常处理器的参数、方法体中定义的局部变量需要使用局部变量表来存放。但是并不是方法中使用的局部变量的总数作为max_locals,因为局部变量表中的slot可以被重复使用。编译器会根据变量的作用域来分类slot并分配给各个变量使用,然后计算出max_locals的大小。
code_length 和 code用来存储字节码指令。code中每一条指令都是u1类型,一共可以表达256条指令,目前java虚拟机规范已经定义了约200条指令的含义。由于code_length 是u4类型但是虚拟机限制一个方法中不允许超过65535条指令,如果超过java编译器会拒绝编译。
在字节码指令之后的是这个方法的显式的异常处理表集合,异常表包含四个字段:如果字节码从第start_pc行到第end_pc行之间出现了类型为catch_type或其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引)则转到handler_pc行继续处理。当catch_type为0是代表任何异常情况都要转向handler_pc处进行处理。异常表格式如下:
异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finallly处理机制。
Exceptions属性
Exceptions 属性作用是列举出方法中可能抛出的受检查异常。其结构表如下:
此属性中的number_of_exceptions项表示方法可能抛出number_of_exceptions中受检异常,每一种受检查的异常使用一个exception_index_table项表示。
LineNumberTable属性
主要用于描述Java源代码行号与字节码行号之间的对应关系。它并不是运行时必须的属性,但默认会生成到Class文件之中,可以在Javac中使用-g:none 或-g:lines选项来取消或要求生成这项信息。其格式如下:
line_number_table是一个数量为line_number_table_length 类型为line_number_info的集合,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。
LocalVariableTable属性
用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,他们不是运行时必须的属性,默认也不会生成到Class文件中。如果没有生成这个属性,最大的影响是当其他引用这个方法时,所有的参数名称都将丢失,IDE可能会使用arg0、arg1等来代替原来的参数名。
其属性表如下:
local_variable_info的结构如下所示:
start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度。name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称及该局部变量的描述符。
index是这个局部变量在栈帧局部变量中slot的位置,当这个变量的数据类型是64位类型时,它占用的slot为index和index+1两个位置。
SourceFile属性
用于记录生成这个Class文件的源码文件名称。这个属性也是可选的。对于大多数类来说,类名和文件名是一致的,但是内部类除外。如果没有这个属性,当抛出异常的时候,堆栈中不会显示出错误代码所属的文件名,其结构如下:
ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量才可以使用这项属性。对于非static类型的变量的赋值是在实例构造器<init>方法中进行的,而对于类变量,有两种方式可选择,赋值在类的构造器<clinit>方法中进行,或者使用ConstantValue属性来赋值。目前Sun javac编译器选择是:如果同时使用static和final来修饰一个变量,并且这个变量的数据类型是基本类型或者是String类型的话,就生成ConstantValue属性来进行初始化,如果这个变量并没有被final修饰,或者是非基本数据类型及字符串类型,则选择在<clinit>方法中进行初始化。
虽然有final关键字才更符合ConstantValue的语义,但虚拟机规范中并没有请只要求字段必须设置ACC_FINAL标志,只要求ConstantValue属性的字段必须设置ACC_STATIC标志,对final关键字的要求是javac编译器自己加入的限制。ConstantValue属性的结构如下:
InnerClasse
记录内部类与宿主类之间的关联。其属性表结构如下:
其中inner_classes_info表的结构如下:
Deprecated及Synthetic属性
这两个属性都是布尔型属性,Deprecated一般对应于代码中@deprecated注释。Synthetic属性代表此字段或方法并不是由Java源码直接产生的,而是由编译器自行添加的。自Java1.5以后可以设置访问标志中的ACC_SYNTHETIC标志位,所有非用户代码生成的类,方法及字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一个。唯一的例外是<init>和<clinit>
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/35259.html