根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这中伪结构中只有两种数据类型:无符号数和表。
无符号数:
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节
和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表:
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯性的以“_info”结尾。表用于描述有层次关系的符合结构的数据,整个Class文件本质上就是一张表。,它由以下数据项构成。
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flages | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。
Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以上表的数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
下面具体分析上表中的数据项的具体含义。
package com.example.demo;
public class Test {
private static String param ;
public static void main(String[] args) {
param = "Hello World";
System.out.println(param);
}
}
将上述代码通过 javac Test .java编译成二进制class文件Test.class,然后将class文件在sublime中打开如下:
一个字节八个比特,就是八个二进制位
四个二进制数最大表示为15,就是一个16进制数,所以八位可以表示成两个16进制的数!
所以一个字节由两个16进制表示
四个二进制数最大表示为15,就是一个16进制数,所以八位可以表示成两个16进制的数!
上图是编译好的字节码文件,我们可以看到一堆16进制的字节。下面针对上图一点一点分析。
*1. 魔数与Class文件的版本
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为扩展名乐意随意地改动。class文件的魔数的获得很有“浪漫气息”,值为:oxCAFEBABE(咖啡宝贝?)。
紧接着魔数的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文件。
例如,JDK1.1能支持版本号为45.0~45.65535的class文件,无法执行版本号为46.0以上的Class文件,而JDK1.2则能支持45.0~45.65535的Class文件。现在,最新的JDK版本为1.7,可生成的Class文件主版本号最大值为51.0
*2.常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量是固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。如上图所示,常量池容量为十六进制数0x0021,即十进制的33,这就代表常量池中有33项常量,索引值范围为1~32。在Class文件格式规范制定时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。Class接口中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始。
常量池中主要存放两大类常量:字面常量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
Java代码在进行Javac编译的时候,在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
常量池中每一项常量都是一个表,在JDK1.7之前共有11中结构各不相同的表结构数据,在JDK1.7中为了更好地支持动态语言调用,又额外增加了3中
(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。
这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量属于哪种常量类型。
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integrer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_CLass_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fielderf_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标识方法句柄 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
之所以说常量池是最繁琐的数据,是因为这14中常量类型各自均有自己的结构。
从上面class文件中常量池的第一项常量(Constant #1),它的标志位是0x0a(对应的十进制为10),查上表的标志列发现这个常量属于CONSTANT_Methodref_info。
通过上表的查询可知CONSTANT_Methodref_info型常量的结构。tag是标志位,它用于区分常量类型;第一个index是一个索引值,它指向常量池中一个CONSTANT_Class_info类型的常量,此常量代表了字段的类或接口描述符,这里的index值为0x0007,查询上表是CONSTANT_Methodref_info;第二个index,值为0x0012,查询上表是CONSTANT_NameAndType_info,是字段或方法的部分符号引用。
Constant #1(一共有32个常量,这是第一个,以此类推…..)
0x0a:从常量类型表中我们发现,第一个数据均是u1类型的tag,16进制的0a是十进制,对应表中CONSTANT_Methodref_info
0x0007: CONSTANT_Methodref_info索引项#7
0x0012: CONSTANT_NameAndType_info索引项#18
Constant #2
0x08:从常量类型表中我们发现,第一个数据均是u1类型的tag,16进制的08是十进制,对应表中CONSTANT_String_info
0x0013:索引项#19
Constant #3
0x09: 查表可知是CONSTANT_Fielderf_info
0x0006:查表可知是CONSTANT_Double_info,对应的索引项是#6
0x0014:全局限定名常量索引为#20
Constant #4
0x09: 查表可知是CONSTANT_Fielderf_info
0x0015:索引项是 #21
0x0016:索引项是 #22
Constant #5
0x0a:查表可知是CONSTANT_Methodref_info
0x0017:索引项是 #23
0x0018:索引项是#24
Constant #6
0x07:查表可知是CONSTANT_CLass_info
0x0019:索引项是#25
Constant #7
0x07:查表可知是CONSTANT_CLass_info
0x001a:索引项是#26
Constant #8
0x01:查表可知是CONSTANT_Utf8_info,查询此常量类型的数据结构可知length的长度5(0x0005),length后面紧跟着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。UTF-8缩略编码与普通UTF-8编码的区别是:从‘\u0001’到‘\u007f’之间的字符(相当于1·127的ASCII码)的缩略编码使用一个字节表示,从’\u0080’到’\u07ff’之间的所有字符的缩略编码用两个字节表示,“\u0800”到‘\uffff’之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示。本例中这个字符串的长度为5字节,往后5字节正好在1~127的ASCII码范围以内:0x70 0x61 0x72 0x61 0x6d 对应的内容为param
Constant #9
0x01:查表可知是CONSTANT_Utf8_info,length=18(0x0012),从当前往后的18个字节正好在1~127的ASCII码范围以内:4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b,对应的内容为Ljava/lang/String;
到此为止,我们分析了Test .class常量池中32个常量中的9个,其余的23个都可以通过类似的方法计算出来。在JDK的bin目录中,Oracle公司已经为我们准备好一个专门用于分析Class文件字节码的工具:javap。下面列出了使用javap工具的-verbose参数输出的Test.class文件字节码内容。
*3. 访问标志
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。具体的标志位以及标志的含义见下表:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDK1.0.2发生过改变,为了区别这条指令使用哪种语意,JDK1.0.2之后编译出来的类的这个标志都必须为真。 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
access_flags中一共有16个标志位可以使用,当前只定义了其中8个,没有用到的标志位要求一律为0。
*4. 类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用来确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引值有一个,除了java.lang.Object之外,所有的Java类有有父类,因此除了java.lang.Object外,所有Java类的父类都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身就是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
*5. 字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段的属性有:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字,字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
字段表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | description_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型,其中可以设置的标志位和含义如下。
字段访问标志:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否public |
ACC_PRIVATE | 0x0002 | 字段是否为private |
ACC_PROTECTED | 0x0004 | 字段是否为protected |
ACC_STATIC | 0x0008 | 字段是否为static |
ACC_FINAL | 0x0010 | 字段是否为final |
ACC_VOLATILE | 0x0040 | 字段是否为volatile |
ACC_TRANSIENT | 0x0080 | 字段是否为transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生的 |
ACC_ENUM | 0x4000 | 字段是否为enum |
跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称以及字段的方法的描述符。现在需要解释一下“简单名称”、“描述符”、以及“全限定名”这三种特殊字符串的概念。
以“com/example/demo”为例,它是“Test”这个类的全限定名,仅仅是吧类名中的“.”替换成了“/”而已,为了是连续的多个全限定名之间不产生混淆,在使用是最后一般会加入一个“;”,表示全限定名结束。简单名称是指没有类型和参数修饰的方法挥着字段名称。
相对于全限定名和简单名称来说,方法和字段的描述符就要复杂一些。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。
描述符标识字符含义:
标识字符 | 含义 |
---|---|
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型shor |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型,如Ljava/lang/Object |
对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为:“[[Ljava/lang/String”,一个整形数组“int[]”将被记录为“[I”。
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数l列表按照参数的严格顺序存放在一组小括号“()”之内,如方法java.lang.String.toString()的描述符为“()Ljava/lang/String;”。
字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问,会自动添加指向外部类实例的字段。还譬如默认无参的构造函数会以字节码的形式显示出来。另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是不是相同。都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。
*6. 方法表集合
Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。
方法表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | description_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法访问标志:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否为public |
ACC_PRIVATE | 0x0002 | 方法是否为private |
ACC_PROTECTED | 0x0004 | 方法是否为protected |
ACC_STATIC | 0x0008 | 方法是否为static |
ACC_FINAL | 0x0010 | 方法是否为final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否为synchronized |
ACC_BRIDGE | 0x0040 | 方法是否为编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_STRICTFP | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否为编译器自动产生的 |
与字段表集合相对应的,如果父类方法在子类没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“”方法和实例构造器“”方法。
在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号应用的集合,也就是因为返回值不会包含在特征签名中,因此Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。当时在Class文件中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。
*7. 属性表(attribute_info)集合
虚拟机规范预定义的属性:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类,方法表,字段表 | 标识方法或字段为编译器自动生成的 |
Code属性
JAVA程序方法体中的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性中。
max_stack代表操作栈深度的最大值,在方法执行的任何时刻,操作数栈都不会超过这个深度,虚拟机运行的时候需要根据这个值来分配栈帧中的操作栈深度。
max_locals代表局部变量表所需的存储空间,单位为slot,对于byte,char,float,int,short,boolean,reference和returnAddress每个局部变量占用一个slot,而double和long需要两个slot.
并不是方法中用到了多少个局部变量,就把这些局部变量所占的Slot之和作为max_locals的值,原因是局部变量表中的slot可以重用,当代码执行超过一个局部变量的作用域时,这个局部变量所占用的slot就可以被其他局部变量所使用。这个值编译器会自动计算得出
code_length和code用来存储java源程序编译后生成的字节码指令,code_length代表字节码长度,code用于存储字节码指令的一系列字节流。字节码的每个指令就是一个字节。这样可以推出,一个字节最多可以代表256条指令,目前已经使用了约200条。而code_length有4个字节,所以一个方法做多允许有65535条字节码指令,如果超过这个限制,javac就会拒绝编译,一般JSP可能会这个原因导致失败。
在任何实例方法中,都可以通过this关键字访问到此方法所属的对象,它的底层实现就是通过javac编译器在编译的时候把this关键字的访问转变为对一个普通方法参数的访问。因此,任何实例方法的参数Args_size最少是1,而且locals最少也是1.而静态方法就可以为0了。异常表实际是Java代码的一部分,从start_pc行到end_pc行出现了类型为catch_type的异常,就转到第handler_pc行处理。这四个参数就组成了异常表。对于finally的实现,实际上就是对catch字段和前面对于任意情况都运行的异常表记录
Exceptions属性
表示方法可能抛出number_of_exceptions种受查异常,每种受查异常使用一个exception_index_table项表示
LineNumberTable属性
用于描述java源代码行号与字节码行号直接的对应关系。可以用-g:none或-g:lines选项来取消或要求生成这项信息,主要影响是报错时堆栈是否显示出错的行号。同时debug时无法设置断点。
LocalVariableTable属性
用于描述栈帧中局部变量表中的变量与源码中定义的变量之间的关系。也可以选择开关,关闭后果就是报错时看不到变量名称
SourceFile属性
用于记录生成这个Class文件的源码文件名称。可选
ConstantValue属性
通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量才可以使用这项属性。
在JAVA中,int x=123;和static int x=123;的区别在于,非static类型的变量(实例变量)的赋值是在实例构造器方法中进行的;而对于静态变量,则有两种方式可以选择:在类构造器方法中进行,或者使用ConstantValue属性来赋值。目前SUN JAVAC编译器的选择是如果同时使用final和static来修饰一个变量,并且这个变量的数据类型是基本类型或者String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有用final修饰,或者非以上类型,则选择在中进行初始化
InnerClasses属性
用于记录内部类与宿主类之间的关系
Deprecated和synthetic属性
都属于标志类布尔属性。deprecated表示在代码中使用@deprecated注释进行设置。synthetic表示字段或方法不是java源码产生,而是编译器自行添加的