深入java虚拟机,了解.class文件

大部分程序员都认为Java虚拟机执行Java程序是一件理所当然和天经地义的事,但时至今日,商业机构和开源机构已经在Java语言之外发展出一大批在Java虚拟机之上运行的语言,如Clojure、Groovy、JRuby、Jython、Scale等。使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等其它语言的编译器一样可以把程序代码编译成Class文件,Java之所以能够跨平台运行,是因为Java虚拟机可以载入和执行同一种平台无关的字节码。也就是说,实现语言平台无关性的基础是虚拟机和字节码存储格式,虚拟机并不关心Class的来源是什么语言,只要它符合Class文件应有的结构就可以在Java虚拟机中运行。

《深入java虚拟机,了解.class文件》

 

Class类文件的结构

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

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

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

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

 

《深入java虚拟机,了解.class文件》

ClassFile表中各项简介如下:

1. Magic(魔数)

每个javaclass文件的前四个字节被称为它的魔数(magicnumber);0xCAFEBABE。魔数的作用在于可以轻松的分辨出Javaclass文件和非Javaclass文件。如果一个文件不是以0xCAFFEBABE开头,那它就肯定不是Javaclass文件。文件格式定义者能够自由选择魔数,前提是这个选定的魔数值没有被广泛应用。当Java还被成为“Oak”的时候,这个魔数就已经定下来了。依照PatrickNaughton(最初Java开发小组的关键成员)的说法:“早在Java第一次作为该语言的名字发布以前,我们就在寻找一些好玩的,唯一的,容易记忆的东西。选择0xCAFEBABE只不过是一个巧合,它象征着著名咖啡品牌Peet’sCoffee中深受欢迎的baristas,它预示了Java这个名字的出现”。

2. Minor_version和major_version

Class文件的下面4个字节包含了主次版本号。随着Java技术的发展,Javaclass文件格式可能会加入新特性。Class文件格式一旦发生变化,版本号也会随之变化。对于Java虚拟机来说,版本号确定了特定的class文件爱你格式,通常只有给定主版本号和一系列次版本号后,Java虚拟机才能够读取class文件。如果class文件的版本号超出了Java虚拟机所能处理的有效范围,Java虚拟机将不会处理该class文件。

在Sun的JDK1.0.2发布版中,Java虚拟机实现支持从45.0(主版本号为45,次版本号为0)到45.3的class文件格式。在所有JDK1.1发布版中的虚拟机都能够支持版本从45.0到45.65535的class文件格式。在Sun的1.2版本的SDK中,虚拟机能够支持从版本45.0到46.0的calss文件格式。

1.0或1.2版本的编译器能够产生版本号为45.3的class文件。在Sun的1.2版本SDK中,javac编译器默认产生版本号为45.3的class文件。但如果在javac命令行中指定了-target1.2标志,1.2版本的编译器将产生版本号为46.0的class文件。1.0或1.1版本的虚拟机上不能运行使用-target1.2标志所产生的class文件。

Java虚拟机实现的第二版中修改了对class文件主版本号和次版本号的解释,对于第二版而言,class文件的主版本号与Java平台主发布版的版本号保持一致,次版本号与特定主平台发布版的各个发布版相关。因此,尽管不同的class文件格式可以由不同的版本号表示,但版本号不一样并不代表class文件格式不同,版本号不同的原因可能只是因为class文件由不同发布版本的Java平台产生的,可能class文件的格式并没有改变。

3. Constant_pool_cound和constant_pool

在class文件中,魔数和版本号后面的是常量池。正如第5章中所述,常量池包含了与文件中类和接口相关的常量。常量池中存储了诸如文字字符串、final变量值、类名和方法名的常量。Java虚拟机把常量池组织为入口列表的形式。在实际列表constant_pool之前,是入口列表中的技术constant_pool_count。

常量池中的许多入口都指向其他的常量池入口,而且class文件中紧随着常量池的许多条目也会指向常量池中的入口。在整个class文件中,指示常量池入口在常量池列表中位置的整数索引都指向这些常量池入口。列表中的第一项索引值为1,第二项索引值为2,以此类推。尽管constant_pool列表中没有索引值为0的入口,但却是这一入口也被constant_pool_count计数在内。例如,当constant_pool中有14项(索引值从1到14)时,constant_pool_count的值为5.

每个常量池入口都从一个长度为一个字节的标志开始,这个标志指出了列表中该位置的常量类型。一旦java虚拟机获取并解析这个标志,Java虚拟机就会知道在标志后的常量类型是什么,表6-3列出了所有常量池标志的名字和值。

表6-3中的每一个标志都有一个相对应的表,表名通过在标志后加上“_info”后缀来产生。例如,对英语CONSTANT_Class标志的表名为CONSTANT_Class_info,表名为CONSTANT_Utf8_info的表中存储着Unicode字符串的压缩形式。对英语各种不同常量池入口的表将在本章后面详细描述。

在动态链接的Java程序中,常量池充当了十分重要的角色。除了字面常量(或者说直接量)值以外,常量池还可以容纳下面几种符号引用。

1. 类和字段的全限定名。

2. 字段的名称和描述符

3. 方法的名称和描述符

字段是类或接口的实例变量或者类变量。字段的描述符是一个指示字段的类型的字符串。方法的描述符也是一个字符串,该字符串指示方法的返回值和参数的数量,顺序和类型。在运行时,Java虚拟机使用常量池的全限定名,方法和字段的描述符,把当前类或接口中的代码与其他类或接口中的代码连接起来。由于class文件并不包含其内部组件最终内存布局的信息,因此类,字段和方法并不能被class文件中的字节码直接引用。Java虚拟机从常量池获得符号引用,然后在运行时解析引用项的实际地址。例如,用来调用方法的字节码指令把一个符号引用的常量池索引传给所调用的方法。

(4)access_flags

紧接常量池后的两个字节成为access_flags,它展示了文件中定义的类或接口的几段信息。例如,访问标志指明文件中定义的是类还是接口;访问标志还定义了在类或接口中的声明中,使用了哪种修饰符;类和接口是抽象的,还是公共的;类的类型可以为final,而final类不可能是抽象的;接口不能为final类型。这些标志位的定义如表6-4所示。

ACC_SUPER标志与Sun的老版本Java编译器向后兼容。Sun当前版本的Java虚拟机中,invokespecial指令的语义比老版本中的更为严格。所有新版本的编译器都必须设置ACC_SUPER标志。所有新的Java虚拟机实现都必须实现更新的、更严格的invokespecial语义。Sun的老版本编译器产生class文件时,将ACC_SUPER标志设为0,即使设定了这个标志,Sun的老版本Java虚拟机也将忽略它

在access_flags中所有未使用的位都必须由编译器置0,而且Java虚拟机必须忽略它。

(5) this_class

接下来的两个字节为this_class项,它是一个对常量池的索引。在this_class位置的常量池入口必须为CONSTANT_Class_info表,该表由两个部分组成——标签和name_index,标签部分是一个具有CONSTANT_Class值的常量,在name_index位置的常量池入口为一个包含了类或接口全限定名的CONSTANT_Utf8_info表

This_class项提供了一个如何使用常量池的范例。对于它自身来说。this_class项只是一个指向常量池的索引。当Java虚拟机在this_class位置查阅常量池入口的时候,它会发现一个通过把自己的标签设为CONSTANT_Class来识别自身的项,Java虚拟机知道,在CONSTANT_Class_info入口中,标签的后面总会有一个名为iname_index的,指向常量池的索引。于是虚拟机在name_index位置查找常量池入口,在这个位置,Java虚拟机应该能找到一个容纳了类或者接口全限定名的CONSTANT_Utf8_info入口,对于这个过程,图6-2有一个图形描述。

(6)super_class

在class文件中,紧接在 this_class之后的是supper_class项,它是一个两个字节的常量池索引。在super_class位置的常量池入口是一个指向该类超类全限定名的CONSTANT_Class_info入口。因为Java程序中所有对象的基类都是java.lang.Object类,除了Object类以外,常量池索引super_class对于所有的类均有效。对于Object类,super_class的值为0.对于接口,在常量池入口super_class位置的项为java.lang.Object。

(7)interfaces_count和interfaces

紧接着super_class的是interfaces_count,此项的含义为:在文件中由该类直接实现或者由接口所扩展的父接口的数量。在这个计数的后面,是名为interfaces的数组,它包含了对每个由该类或者接口直接实现的父接口的常量池索引。每个父接口都使用一个常量池中的CONSTANT_Class_info入口来描述,该CONSTANT_Class_info入口指向接口的全限定名。这个数组只容纳那些直接出现在类声明的implements子句或者接口声明的extends子句中的父接口。超类按照在implements子句和extends子句中出现的顺序(从左到右)在这个数组中显现。

(8)fields_count和fields

在class文件中,紧接在interfaces后面的是对在该类或者接口中所声明的字段的描述。首先是名为fields_count的计数,它是类变量和实例变量的字段的数量总和。在这个计数后面的是不同长度的field_info表的序列(fields_count指出了序列中有多少个field_info表)。只有在文件中由类或者接口声明了的字段才能在fields列表中列出。在fields列表中,不列出从超类或者父接口继承而来的字段。另一方面,fields列表可能会包含在对应的Java源文件中没有叙述的字段,这是因为Java编译器可能会在编译时向类或者接口添加字段。例如,对于一个内部类的fields列表来说,为了保持对外围实例的引用,Java编译器会为每个外围类实例添加实例变量。源代码中并没有叙述任何在fields列表中的字段,它们是被Java编译器在编译时添加进去的,这些字段使用Synthetic属性标识。

每个field_info表都展示了一个字段的信息。此表包含了字段的名字、描述符和修饰符。如果该字段被声明为final,field_info表还会展示其常量值。这样的信息有些放在field_info表中。有些则放在由field_info表所指向的常量池中。

(9)method_count和methods

在class文件中,紧接着fields后面的是对在该类或者接口中所声明的方法的描述。首先是名为methods_count的计数,它是一个双字节长度的对于该类或者接口中声明的所有方法的总计数。这个总计数只包含在该类或者接口中显式定义的方法(从超类或者父接口中继承来的方法不被计入)。在methods_count后面的是方法本身,它在一个method_info表的列表中进行了阐述(method_count指出了列表中有多少个method_info表)。

Method_info表中包含了与方法相关的一些信息,包括方法名和描述符。方法的返回值类型和参数类型。如果方法既不是抽象的,又不是本地的,那么method_info表就包含了方法局部变量所需的栈空间长度、为方法所捕获的异常表、字节码序列以及可选的行数和局部变量表。如果方法能够抛出任何已验证的异常,那么method_info表就会包括一个关于这些已验证异常的列表。

(10)attribute_count和attributes

Class文件中最后的部分是属性(attribute),它给出了在该文件中类或者接口所定义的属性的基本信息。属性部分由attributes_count开始,attributes_count是指出现在后续attributes列表中的attribute_info表中的数量总和。每个attribute_info的第一项是指向常量池中CONSTANT_Utf8_info表的索引,该表给出了属性的名称。

属性有许多种。Java虚拟机规范定义了几种属性,但任何人都可以创建他们自己的额属性种类(通过特定的规则),并且把它们置于class文件中。Java虚拟机实现必须忽略任何不能识别的属性。创建新属性种类的规则将在本章后面。

属性出现在class文件中的多处,而不仅仅在顶层ClassFile表的attributes项中出现。出现在ClassFile表中的属性主要给出了与文件中所定义的类和接口相关的信息;出现在field_info表中的属性主要给出了与字段相关的信息;出现在method_info表中的属性主要给出了与方法相关的信息。

Java虚拟机实现定义了两种属性——SourceCode和InnerClasses,它们出现在ClassFile表中属性列表中。

 

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/haorantiangang/article/details/9429503
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞