Java二进制Class文件格式解析
一、Java Class文件是什么
《The JavaTM Virtual Machine Specification》(Second Edtion)中有表述:Java Class文件由8位字节流组成,所有的16位、32位和64位数据分别通过读入2个、4个和8个字节来构造,多字节数据总是按照Big-endian顺序来存放,即高位字节在前(放在低地址)。每个Class文件都包含且仅包含一个Java类型(类或者接口)。
或许,《The JavaTM Virtual Machine Specification》中的表述不够明确,那么我们可以参考一下《Inside the Java Virtual Machine》(Second Edtion)中的表述:Java Class文件特指以。class为后缀名的Java虚拟机可装载的文件。
分析一下两者的表述,我觉得都不够全面、不够明确。我是这么定义的:Java Class文件就是指符合特定格式的字节流组成的二进制文件。这个特定的格式就是指第二节要讨论的Class文件格式,亦即在《The JavaTM Virtual Machine Specification》中定义的Class文件格式。从另一个角度来说,这个特定格式就是指JVM能够识别、能够装载的格式。为什么这么说呢?因为JVM在装载class文件时,要进行class文件验证,以保证装载的class文件内容符合正确的内部结构。这个内部结构指的就是这个特定格式,只要是符合这个特定格式的Class文件都是合法的、规范的Class文件,都是JVM能够装载的Class文件。如果觉得这样的表述还是不够明确,我只能建议你读完这篇文章之后再回头来理解看看了托福答案
为了讨论方便,在下文中将对这两个参考资料做个简记:
1)《The Java Virtual Machine Specification》(Second Edtion)简记为《JVM Spec》(2nded)。
2)《Inside the Java Virtual Machine》(Second Edtion) 简记为《Inside JVM》(2nded)。
二、Java Class文件的格式
在讲Class文件的格式之前,要介绍三个概念:
1)数据类型:《JVM Spec》(2nded)中指出,Java Class文件的数据用自己定义的一个数据类型集来表示,即u1,u2,u4,分别用于表示一个无符号类型的、占1,2,4个字节的数据。在《Inside JVM》(2nded)一书中,作者把这个数据类型集称之为Class文件的基本类型,本人觉得比较形象,便于理解。所以,在本文中,我们也用基本类型来表示Java Class文件的数据。
2)表:根据《JVM Spec》(2nded)中的定义,表(table)由项(定义见3)组成,用于几种Class文件结构中。《JVM Spec》(2nded)中指出,Java Class文件格式用一个类似于C结构的记号编写的伪结构来表示。这个伪结构指的就是这里的表,例如下面的ClassFile表就是这种伪结构的一个典型例子,下文中所有的表都是指这种伪结构的表。表的大小是可变的,这是因为它的组成部分项是可变的。注意;这里的可变是针对Class层次而言的,即在不同的Class文件中该项的大小可能不一样的,但是对于每一个具体的Class文件来说,这个项的大小又是一定的,因而这个表的大小也是一定的。那么,项为什么是可变的呢?请看下面的分析。
3)项:描述Java Class文件格式的结构的内容称为项(items)。每个项都有自己的类型和名称。项的类型可能是基本类型,也可能是一个表的名字,这种项都是一些数组项。数组项的每一个元素都是一个表,这个表同顶层的ClassFile表一样,也都是一种伪结构,也都是由一些项构成的,而且这些表不一定是同一种格式的,因此数组项也可以看作一个可变大小的结构流J.这些表对于该数组项来说就是子项,当然子项可能还有子项(目前子项的深度最多就两层)。项的名称,没有什么好说的,就是《JVM Spec》(2nded)中指定的一些名称。另外,项也是有大小的,对于没有子项的项来说,其大小是固定的;对于有子项的项来说,其大小是可变的。在一个具体的Class文件中,一个可变项(数组)的大小都会在其前一项中指定,为什么会是这样的呢?因为《JVM Spec》(2nded)中就是这么定义的!在Class文件中,每个项按规范中定义好的顺序存储在Class文件中,相邻的项之间没有任何间隔,连续的项(数组)也是按顺序存储,不进行填充或者对齐,这样可以使Class文件紧凑。
好了,我想这三个概念我已经解释地比较清楚了,下面开始正式解析Class文件的格式。
首先要来解析一下ClassFile表结构,这是《JVM Spec》(2nded)中定义的Class文件最外层的结构,换言之,就是Class文件的格式。
ClassFile表结构
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_Class;
u2 super_Class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
ClassFile表结构由16个不同的项组成,其中的各项可以简要地分析如下:
(1) magic
每个Class文件的前4个字节被称为它的魔数(magic number): 0xCAFEBABE.魔数的作用在于:可以轻松地分辨出Java Class文件和非Java Class文件。(如果一个文件不是以0xCAFEBABE开头,它就肯定不是Java Class文件,因为它不符合规范J)。当Java还称为"Oak"的时候,这个魔数就已经定下来了,它预示了Java这个名字的出现。魔数的来历请大家自己查阅J
(2) minor_version和major_version
Class文件的下面4个字节包含了次、主版本号。通常只有给定主版本号和一系列次版本号后,Java虚拟机才能够读取Class文件。如果Class文件的版本号超出了Java虚拟机所能够处理的有效范围,Java虚拟机将不会处理该Class文件。例如J2SE5.0版本的虚拟机就不能执行由J2SE6.0版本的编译器编译出来的Class文件。北美托福答案
(3) constant_pool_count
版本号后面的项是constant_pool_count即常量池计数项,该项的值必须大于零,它给出该Class文件中常量池列表项的元素个数,这个计数项包括了索引为0的constant_pool表项,但是该表项不出现在Class文件的constant_pool列表中,因为它被保留为Java虚拟机内部实现使用了,因此常量池列表的元素个数constant_pool_count-1,各个常量池表项的索引值分别为1到constant_pool_count-1.
注:在这里,有几个术语需要解释一下,常量池即为constant_pool,常量池列表就是指constant_pool[ ],常量池表项即指常量池列表中的某一个具体的表项(元素)。这些常量池表项的可能类型如下述的cp_type表所示:
cp_type
入口类型 标志值
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
(4) constant_pool[ ]
constant_pool_count项下面是constant_pool[ ]项,即常量池列表,其中存储了该ClassFile结构及其子结构中引用的各种常量,诸如文字字符串、final变量值、类名和方法名等等。在Java Class文件中,常量池表项是用一个cp_info结构来描述的,常量池列表就是由constant_pool_count-1个连续的、可变长度的cp_info表结构构成的constant_pool[ ]数组。为什么是constant_pool_count-1个constant_pool的原因,在上面已经解释了。每一个常量池表项都是一个变长结构,其通常格式如下所示:
cp_info
cp_info表的tag项是一个无符号的byte类型值,它表明了cp_info表的类型和格式,具体的tag类型见上表。
需要说明的是,cp_info只是一个抽象的概念,在Class文件中,它表现为一系列具体的、形如CONSTANT_Xxxx_info的constant_pool结构,其具体的格式由cp_info表的tag项(即第一个字节)来确定。不同的cp_info表,其info[]项也是不一样的,例如,CONSTANT_Class_info表的info[]项为"u2 name_index",而CONSTANT_Utf8_info表的info[]项为"u2 length; u1 bytes[length];",显然,这两个cp_info表是不一样的,大小更是不一样的,因而常量池表项的大小是可变的。由于常量池列表中的每个常量池表项的结构是不一样,因此常量池列表的大小也是可变的。在Class文件中,常量池列表项是一个可变长度的结构流。
由cp_info表以及cp_type表我们可以知道,若cp_info表中tag(标志)项的值为1时,当前的cp_info就是一个CONSTANT_Utf8_info表结构,若cp_info表中tag项的值为3,当前的cp_info就是一个CONSTANT_Integer_info表结构,其它情况类推。这些表的结构可以查阅《JVM Spec》(2nded)的第四章或者《Inside JVM》(2nded)的第六章。托福考前答案
(5) access_flags
紧接常量池后的两个字节称为access_flags,access_flags项描述了该Java类型的一些访问标志信息。例如,访问标志指明文件中定义的是类还是接口;访问标志还定义了在类或接口的声明中,使用了哪些修饰符;类和接口是抽象的还是公共的等等。实际上,access_flags项的值是Java类型声明中使用的访问标志符的掩码(mask,这里掩码指的是access_flags的值是所有访问标志值的总和,当然,未被使用的标志位在Class文件中都被设置为0.例如,若access_flags的值就是0x0001,就表示该Java类型的访问标志符是ACC_PUBLIC;若access_flags的值是0x0011,就表示该Java类型的访问标志符是ACC_PUBLIC和ACC_FINAL,因为只有这两个标志位的和才可能是0x0011;其它情况类推)。
一个Java类型的所有access_flags标志符如下表所示:
access_flags
标志名称 值 含义
ACC_PUBLIC 0x0001 声明为public,可以从它的包外访问
ACC_FINAL 0x0010 声明为final,不允许有子类
ACC_SUPER 0x0020 用invokespecial指令处理超类的调用
ACC_INTERFACE 0x0200 表明是一个接口,而不是一个类
ACC_ABSTRACT 0x0400 声明为abstract,不能被实例化
需要说明的是,这是针对一个Java类型的访问标志符列表,有的标志符只有类可以使用,有的标志符只有接口才可以使用,
(6) this_class
接下来的两个字节为this_class项,其值为一个对常量池表项的索引,即它指向一个常量池表项,而且该常量池表项必须为CONSTANT_Class_info表的结构。该表有一个name_index项,该项将指向另一个常量池表项,该表项包含了该类或者接口的完全限定名称。
(7) super_class
紧接着this_class之后的两个字节是super_class项,该项必须是对常量池表项的一个有效索引或者值为0.如果super_class项的值为0,则该Class文件必须表示java.lang.Object类。如果super_class项的值不为0,则又分为两种情况,若该Class文件表示一个类,则super_class项必须是对常量池中该类的超类的CONSTANT_Class_info表项的索引,这个超类和它的任何超类都不能是一个final类;若该Class文件表示一个接口,则super_class项必须是对常量池中表示java.lang.Object类的一个CONSTANT_Class_info表项的索引。美国托福答案
(8) interfaces_count和interfaces[ ]
紧接着super_class项后面的两个字节是interfaces_count项,此项表示由该类直接实现或者由该接口所扩展的超接口的数量。
紧接着interfaces_count项后面的是interfaces列表项,它包含了由该类直接实现或者由该接口所扩展的超接口的常量池索引,共计interfaces_count个索引。interfaces列表中的常量池索引按照该类型在源代码中给定的从左到右的顺序排列。
(9) fields_count和fields[ ]
接下来的是fields_count项,该项的值给出了fields列表项中的field_info表结构的数量,即表示了该Java类型声明的类变量和实例变量的个数总和。
fields列表项包含了在该Java类型中声明的所有字段的完整描述。fields列表中的每个field_info表项都完整地表示了一个字段的信息,包括该字段的名称、描述符和修饰符等。这些信息有的放在field_info表中,如修饰符;有的则放在field_info表所指向的常量池中,如名字和描述符。同前面的分析,fields列表项也是一个变长结构。
需要说明的是,只有在该Java类型中声明的字段才可能在fields列表中列出,fields列表中不包括从超类或者超接口中继承而来的字段信息。
(10) methods_count和methods[ ]
在Class文件中,紧接着fields后面的是对在该Java类型中所声明的方法的描述。首先是methods_count项,它占两个字节长度,它的值表示对该Java类型中声明的所有方法的总计数。methods_count项后面是methods列表项,它由methods_count个连续的method_info表构成。每个method_info表都包含了与一个方法相关的信息,如方法名、描述符(即方法的返回值及参数类型)以及一些其它信息。如果一个方法既非abstract也非native,那么该method_info表将包含该方法局部变量所需的栈空间长度、为方法所捕获的异常表、字节码序列以及可选的行号表和局部变量表等信息。
需要说明的是,只有在该Java类型中显式定义的方法才可能在fields列表中列出,fields列表中不包括从超类或者超接口中继承而来的方法信息。
(11) attributes_count和attributes[ ]
Class文件中最后的部分是属性(attribute),它给出了在该Java类型中所定义的属性的基本信息。首先是attributes_count项,它占两个字节长度,它的值表示在后续的attributes列表中的attributes_info表的总个数。每个attributes_info表的第一项都是对常量池中CONSTANT_Utf8_info表项的一个索引,该表给出了此属性的名称。托福改分
需要说明的是,属性有很多种,在Class文件中的很多地方都出现了属性这一项,在顶层ClassFile表中有attributes属性项,在field_info表中也有attributes属性项,在method_info中也有attributes属性项,但是它们各有各的功能,详见上述分析。在《JVM Spec》(2nded)中,为ClassFile表结构的attributes列表项定义的唯一属性是SourceFile属性,为field_info表结构的attributes列表项定义的唯一属性是ConstantValue属性,为method_info表结构的attributes列表项定义的属性是Code属性和Exceptions属性。
总而言之,Class文件格式是一个规范性的格式。这个规范指的就是,上面提到的这些表结构本身的规范性,以及这些表结构之间的包含关系的规范性。实际上,《JVM Spec》(2nded)中就是通过表和项这两个概念来组织Class文件的格式的。首先,ClassFile表就是Class文件最外层的结构,换言之,这就是Class文件的格式。其次,ClassFile表又是一些项组成的,这些项的内容都要符合《JVM Spec》(2nded)中定义的规范,具体来说,若这个项的类型是基本类型,该项的值要符合规范,例如magic项一定要是0xCAFEBABE,access_flags项的值一定要是有效的标志值等等;若这个项的类型是一个表名,即该项是一个数组项,那么该数组项列表中的每一个表项都要是一个合法的、规范的表,不能是一个规范中没有定义的新表,这就是包含关系的规范性,同样,列表项中的每个表项本身也都要是符合其规范定义的表项,例如常量池列表中的某个CONSTANT_Class_info表的name_index项不是对一个CONSTANT_Utf8_info表结构的索引,那么这个常量池的表项就不是一个合法的表项,因而这个常量池列表项就是不符合规范的,因而整个文件就是不符合规范的。