类文件结构

来源:互联网 发布:财会书籍知乎 编辑:程序博客网 时间:2024/04/29 19:32

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

6.1概述

       计算机只认0和1

6.2 无关性的基石

        各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石。

        实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件“这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。


6.3  Class 类文件的结构

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

       Class文件是一组以8位字节为基础单位的二进制流。

        根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。

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

       表是由多个无符号数或者其他表作为数据项构成的复合数据类型。


6.3.1 魔数和Class文件的版本

       每个Class文件的头四个字节成为魔数(Magic Number),它的唯一作用是确认这个文件是否为一个被虚拟机接受的Class文件。Java:0xCAFEBABE。紧接着魔数的四个字节存储的的是Class文件的版本号:第5和第6个字节是次版本号,第七个和第八个字节是主版本号。


       前四个字节是魔数”0xCAFEBABE“,此版本号是0x0000,主版本号是0x0032。

6.3.2 常量池

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

       由于常量池中常量的数量是不固定的,所以常量池的入口需要放置一项u2类型的数据,代表着常量池容量计数值(constant pool count)。它与Java语言习惯不一样,它是从1开始计数的。如上图中0x0016,即十进制的22,这就代表着常量池中有21个常量。Class文件中只有常量池的容量计数是从1开始的。

       常量池主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值。而符号引用则属于编译原理方面的概念,包括下面三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

       常量池中每一项常量都是一个表。


       如上面字节码常量池的第一项常量,它的标志位是(0x0000000A)是0x07,查表6-3得知发现这个是个CONSTANT_Class_info类型,此类型的常量代表一个类或者接口的引号引用。CONSTANT_Class_info的结构比较简单。

       tag是标志位,它用于区别常量的类型;name_index是一个索引值,她指向常量池中一个CONSTANT_Utf8_info类型的常量,此常量代表这个类(接口)的全限定名,这里的name_index值(偏移地址:0x0000000B)为0x0002,也即是指向了常量池的第二项常量。继续查找第二项常量,它的标志位(0x000000D)是0x01,查表6-3得知确实是一个CONSTANT_Utf8_info类型的常量。


       length值说明了这个UTF-8编码的字符串长度是多少字节,他后面紧跟着的长度为lingth字节的连续数据是一个使用UTF-8缩略编码表示的字符串。本例中这个字符串的length值(0x0000000E)为0x001D,也就是长为29字节,往后的29个字节就是内容。


      上面可以看出,计算机已经帮我们把21个常量都计算了出来,并且第1、2项常量的计算结果与我们计算的结果一致。

6.3.3 访问标志

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


       代码清单的TestClass是一个普通的Java类,只用到了ACC_PUBLIC和ACC_ENUM标志位,它们的标志为真。因为access_flags的值应为:0x0001|0x0020=0x0021。


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

       类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,class文件中有这三项确定这个类的继承关系。类索引确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。接口索引集合就用来描述这个类实现了哪些接口。

       类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引引用两个u2类型的索引值表示,他们都指向CONSTANT_Class_info的类描述符常量。通过CONSTANT_Utf8_info类型的常量中的全限定名字符串。

       对于接口索引集合,如果的第一项——u2类型的数据为接口计数器,表示索引表的容量。如果该类没有实现任何接口,则该计数器的值为0.





       从偏移地址0x00000F1开始的3个u2类型的值分别为0x0001、0x003、0x0000,也就是类索引为1,父类索引为3,接口索引集合大小为0。

6.3.5 字段表集合

       字段表用于描述接口或者类声明中的变量。字段包括类级变量以及实例级变量,但不包括方法内部声明的局部变量。可以包括的信息为:字段的作用域(public、private、protected修饰符)、实例变量还是类变量(static修饰符)、可变性(final)等。而字段叫什么名、字段被定义为什么数据类型、这些都是无法固定的,只能引用常量池中的常量进行描述。


       字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常相似的,都是一个u2的数据类型,其中可以设置的标志位和含义如下图所示


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

       描述符的作用是用来描述字段的数据类型、方法的参数列表(数量、类型、顺序)和返回值。根据描述符的规则,基本数据类型以及代表无返回值的void都可以用一个大学字母来表示,而对象类型则用字符L加对象的全限定名来表示。


       对于数组类型,每一维度将使用一个前置的”[“字符来描述,如一个定义为”java.lang.String[][]“类型的二维数组,将被记录为”[[Ljava\lang\Strintg“,一个整型数组”int[]“将被记为”[I“.

       用描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号”()“之内。如方法void inc()的描述符为”()V“,方法java.lang.String.toString()的描述符为”()Ljava/lang/String;“.


       字段表集合从地址0x000000F8开始,第一个u2类型的数据为容量计数器fields_count,其值为0x0001,说明这个类只有一个字段表数据。接下来的紧跟着容量计数器的是access_flags标志,值为0x0002,代表着private修饰符的ACC_PRIVATE标志位为真,其他的修饰符为假。代表字段名称的name_jindex的值为0x005,从常量池表中可以查到第5项常量是一个CONSTANT_Utf8_info类型的字符串,其值为”m“,代表字段描述符的description_index的值为0x0006,指向常量池的字符串”I“,根据这些信息我们可以推断出”private int m“。

       字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原来Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

6.3.6  方法表的集合

       Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributres)几项。


       因为volatile关键字和transient关键字不能修饰方法,所有没有这两个的标志。


       方法的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里面,属性表作为Class文件格式中最具扩展性的一个数据项目。

       方法表集合的入口地址是0x00000101,第一个u2类型的数据(即是计数器容量)的值为0x0002,代表集合中有两个方法(实例构造器<init>和源码中的inc())。第一个方法的访问标志位0x0001,也就是只有ACC_PUBLIC标志为真,名称索引为0x0007,查常量池得方法名为”<init>“,描述符索引值为0x0008,对应常量池为”()V“,属性表计数器attributes_count的值为0x0001就表示此方法的属性表集合有一项属性,属性名称为0x0009,对应的常量为Code,说明此属性是方法的字节码描述。

       与字段表集合相对应的,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的就是类构造器。

6.3.7 属性表集合

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


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


1、code属性

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


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

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

       max_locals代表了局部变量表所需的存储空间。在这里max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个变量都占用一个Slot,而double和long这两种64的数据类型则需要两个Slot来存放。

       code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代码字节码的长度,code用于存储字节码指令的一系列字节流。

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

       实例 TestClass.class文件为例

       这是<init>方法中的Code属性。它的操作数栈的最大深度和本地变量表的容量都为0x0001,字节码区域所占空间的长度为0x0005。虚拟机读取到字节码的长度后,按照顺序依次读入紧随的5个字节,并根据字节码指令表翻译出所对应的字节码指令。

6.4 字节码指令简介

       Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(成为操作数Operands)而构成的。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。

       字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构,由于限制了Java虚拟机操作码的长度为一个字节(0~255),这意味着指令集的操作码总数不可能超过256条;由于Class文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时从字节中重建出具体数据的结构,如果将一个16位长度的无符号整数使用两个无符号字节存储起来(将它们命名为byte1和byte2),那他们的值应该是这样的:

(byte1 <<8)| byte2

       这种操作在某种程度上会导致解释执行字节码时损失一些性能。但这样的优势也是非常明显的,放弃了操作数长度对齐,就意味着可以省略很多填充和间隔符号里有一个字节来表示操作码,也是为了尽可能获得短小精干的编译代码。

6.5公有设计和私有实现

       Java虚拟机描绘了Java虚拟机应有的共同程序存储格式:Class文件格式以及字节码指令集。这些内容与硬件、操作系统以及具体的Java虚拟机实现直接是完全独立的,虚拟机实现者可能更愿意把它们看作是程序在各种Java平台实现之间互相安全的交互手段。

       虚拟机实现的方式由主要一下两种:

       ① 将输入的Java虚拟机代码在加载或者执行时翻译成另外的一种虚拟机的指令集。

       ② 将输入的Java虚拟机代码在加载或执行时翻译成宿主机CPU的本地指令集(即JIT代码生成技术)

 


0 0
原创粉丝点击