这篇文章我们来讲一下jvm的类文件结构
目录
[9.2 Exception属性](#9.2 Exception属性)
1.引言
代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一 大步。
任何一个计算机相关专业或者不仅仅是计算机相关专业的人都了解这样一个事实:"计算机只认识0和1,所以我们写的程序需要被编译 器翻译成由0和1构成的二进制格式才能被计算机执行。计算机从诞生到现在已经过去了很多年,但是今天的计算机仍然只能识别 0 和1,但由于最近十年内虚拟机以及大量建立在虚拟机之上的程序语言如雨后春笋般出现并蓬勃发展,把我们编写的程序编译成二进制本地机器码已不再是唯一的选择,越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。
Java在刚刚诞生之时曾经提出过一个非常著名的宣传口号"一次编写,到处运行 (Write Once,Run Anywhere)",这句话充分表达了当时软件开发人员对冲破平台界限的渴求。"与平台无关"的理想最终只有实现在操作系统以上的应用层:Oracle公司以及其他虚拟机发行商发布过许多可以运行在各 种不同硬件平台和操作系统上的Java虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节 码,从而实现了程序的"一次编写,到处运行"。
各种不同平台的Java虚拟机,以及所有平台都统一支持的程序存储格式------字节码(Byte Code) 是构成平台无关性的基石。
实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与"Class文件"这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。基于安全方面的考虑,《Java虚拟机规范》中要求在Class文件必须应用许多强制性的语法和结构化约束,**但图灵完备的字节码格式,保证了任意一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。**作为一个通用的、与机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为他们语言的运行基础,以Class文件作为他们产品的交付媒介。例如,使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等其他语言的编译器一样可以把它们的源程序代码编译成Class文件。虚拟机丝毫不关心Class的来源是什么语言,它与程序语言之间的关系如下图所示:
简单来说:java虚拟机只认.class文件,不认其他的东西。不管你是什么语言,只要你最后被编译成了.class文件,那么jvm就能运行你。至于怎么运行,那就是jvm与操作系统的事了。
2.类文件结构概论
解析Class文件的数据结构是本篇的最主要内容。但是,对文件格式、结构方面的学习,有点类似于"读字典",读者阅读本章时,大概会不可避免地感到 比较枯燥,但这部分内容又是Java虚拟机的重要基础之一,是了解虚拟机的必经之路,如果想比较深入地学习虚拟机相关知识,这部分是无法回避的。下面,我们具体的来看一下
如下图所示,这是一个很简单的java程序:
然后,我们执行javac命令,将其编译为**.class文件**,即二进制字节码文件,然后再看一下:
当然,内容很多,我没有截取完毕。
我们的jvm运行的就是这种二进制字节码文件,java语言的跨平台性的基础就是任何一个java语言都是被编译成这种二进制字节码文件,然后都可以被jvm运行的。我们研究类文件结构就是研究上面的二进制字节码文件的结构特点的。
然后,根据jvm规范,类文件结构如下所示:
下面就来详细的看一下一个类的各个部分(即类文件的各个部分)
3.魔数与class文件的版本
每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为 一个能被虚拟机接受的Class文件。不仅是Class文件,很多文件格式标准中都有使用魔数来进行身份识别的习惯,譬如图片格式,如GIF或者JPEG等在文件头中都存有魔数。**使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意改动。**文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过而且不会引起混淆。Class文件的魔数取得很有"浪漫气息", 值为0xCAFEBABE(咖啡宝贝?)。这个魔数值在Java还被称作"Oak"语言的时候(大约是1991年前后)就已经确定下来了。它还有一段很有趣的历史,据Java开发小组最初的关键成员Patrick Naughton 所说:"我们一直在寻找一些好玩的、容易记忆的东西,选择0xCAFEBABE是因为它象征着著名咖啡品牌Peet's Coffee深受欢迎的Baristas咖啡。" 这个魔数似乎也预示着日后"Java"这个商标名称的出现。
紧接着魔数的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文件,因为《Java虚拟机规范》在Class文件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class 文件。
下面来看一下实例:
如上两图所示,我们可以很清楚的看到魔数和版本号
4.常量池
紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class 文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常 量池容量计数值。与Java中语言习惯不同,这个容量计数是从1而不是0开始的,如下图所示,常量池容量(偏移地址:0x00000008)为十六进制数0x0016,即十进制的22,这就代表常量池中有21项常量,索引值范围为1~21。在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达"不引用任何一个常量池项目"的含义,可以把索引值设置为0来表示。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始。
常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:
- 被模块导出或者开放的包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
Java代码在进行Javac编译的时候,并不像C和C++那样有"连接"这一步骤,而是在虚拟机加载Class 文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。**当虚拟机做类加载时,将会从常量池获得对应的符号 引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。**关于类的创建和动态连接的内容, 在下一篇介绍虚拟机类加载过程时再详细讲解。
5.访问标志
在常量池结束之后,紧接着的2个字节代表访问标志( access_flags**)** ,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract 类型;如果是类的话,是否被声明为final;等等。具体的标志位以及标志的含义见下图:
access_flags中一共有16个标志位可以使用,当前只定义了其中9个,没有使用到的标志位要求一律为零。
6.类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合 (interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,**因此除了 java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,**这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是 extends关键字)后的接口顺序从左到右排列在接口索引集合中。
7.字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的"字段"(Field)包括类级变 量以及实例级变量,但不包括在方法内部声明的局部变量。读者可以回忆一下在Java语言中描述一个字段可以包含哪些信息。字段可以包括的修饰符有字段的作用域(public、private、protected修饰 符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否 强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、 字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫做什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。下图列出了字段表的最终格式:
字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型,其中可以设置的标志位和含义如下图所示:
很明显,由于语法规则的约束,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最 多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,这些都是由Java本身的语言规则所导致的。
8.方法表集合
Class文件存储 格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依 次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表 集合(attributes)几项,如下图所示。这些数据项目的含义也与字段表中的非常类似,仅在访问标志和属性表集合的可选项中有所区别:
因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了 ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对,synchronized、native、strictfp和abstract 关键字可以修饰方法,方法表的访问标志中也相应地增加了ACC_SYNCHRONIZED、 ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。对于方法表,所有标志位及其取值可参见下图:
行文至此,也许有的读者会产生疑问,方法的定义可以通过访问标志、名称索引、描述符索引来 表达清楚,但方法里面的代码去哪里了?方法里的Java代码,经过Javac编译器编译成字节码指令之 后,存放在方法属性表集合中一个名为"Code"的属性里面,属性表作为Class文件格式中最具扩展性的 一种数据项目,将在下一节中详细讲解。
9.属性表集合
属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以 携带自己的属性表集合,以描述某些场景专有的信息。
与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,**属性表集合的限制稍微宽松一 些,不再要求各个属性表具有严格顺序,**并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。
这里只列部分属性表中元素来进行展示:
对于每一个属性,它的名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示, 而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。 一个符合规则的属性表应该满足下图所定义的结构:
9.1code属性
下面来看一下属性表中的code属性。
Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。 Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽 象类中的方法就不存在Code属性,如果方法表有Code属性存在,那么它的结构将如下图所示:
attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,此常量值固定为"Code",它代表了该属性的属性名称,attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6个字节,所以属性值的长度固定为整个属性表长度减去6个字节。
max_stack代表了操作数栈(Operand Stack)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。
max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是变量槽(Slot) ,**变量槽是虚拟机为局部变量分配内存所使用的最小单位。**对于byte、char、float、int、short、boolean和 returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64位的数据类型则需要两个变量槽来存放。方法参数(包括实例方法中的隐藏参数"this")、显式异常处理程序的参数(Exception Handler Parameter,就是try-catch语句中catch块中所定义的异常)、方法体中定义的局部变量都需要依赖局部变量表来存放。
**注意:**并不是在方法中用了多少个局部变量,就把这些局部变量所占变量槽数量之和作为max_locals的值,操作数栈和局部变量表直接决定一个该方法的栈帧所耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存的浪费。Java虚拟机的做法是将局部变量表中的变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量槽可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配变量槽给各个变量使用,根据同时生存的最大局部变量数量和类型计算出max_locals的大小。
code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度, code是用于存储字节码指令的一系列字节流。既然叫字节码指令,那顾名思义每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指 令,并且可以知道这条指令后面是否需要跟随参数,以及后续的参数应当如何解析。
Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法 体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整 个Class文件里,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。
9.2 Exception属性
这里的Exceptions属性是在方法表中与Code属性平级的一项属性,读者不要与前面刚刚讲解完的异常表产生混淆。Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常。它的结构如下图所示:
此属性中的number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常,每一种受 查异常使用一个exception_index_table项表示;exception_index_table是一个指向常量池中 CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。
类的属性表中还有其他许多属性,这里就不一一列举了。具体的内容可以去找专业的书籍去参考。
10小结
这篇文章,我们讲解了java的类文件结构,这是比较偏底层的一些内容,需要扎实的计算机和java的基础,并且很多都是前人写好的规范,需要我们去根据具体的代码去学习,所以,本篇文章只是简略的介绍了一下,让大家知道java的类在编译成字节码文件后到低是什么样的就行,以后遇见了能认出来就可以了。至于更深层次的内容,大家还是去看专业书籍学习吧。