1. 概述
如果计算机的CPU指令集就只有x86一种,操作系统就只有Windows一种,那也许就不会有Java语言的出现。Java在刚刚诞生之时曾经提出过一个非常著名的宣传口号:“一次编写,到处运行(Write Once,Run Anywhere)”,这句话充分表达了软件开发人员对冲破平台界限的渴求。在无时无刻不充满竞争的IT领域,不可能只有Wintel存在,我们也不希望只有Wintel存在,各种不同的硬件体系结构和不同的操作系统定将会长期并存发展。“与平台无关”的理想最终实现在操作系统的应用层上:Sun公司及其他虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现程序的“一次编写,到处运行”。
2. Class类文件的结构
Class的结构不想XML等描述语言,由于它没有任何分隔符号,所以上述的数据项,无论是顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变
2.1 魔数与Class文件的版本
每个Class文件的头4个字节称为魔数(Magic Number)
,它的唯一作用是用于确定这个文件是否为一个能被虚拟机接收的 Class 文件。
这个魔数为:0xCAFEBABE
第5和第6个字节是次版本(minor_version)
第7和第8个字节是主版本号(major_version)
查看一个class文件,结果如下:
下表列举了从JDK1.1到1.7之间,主流JDK版本编译器输出的默认和可支持的Class文件版本号。
2.2 常量池
紧接着主次版本号之后的是常量池入口,是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时也是Class文件中第一个出现的表类型数据项目
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中的语言习惯不一样的是,这个容量计数是从1而不是0开始的,如图6-3所示,常量池容量(偏移地址:0x00000008)为十六进制数0x0016,即十进制的22,这就代表常量池中有21项常量,索引值为1~21。制定Class文件格式规范时,将第0项常量空出来是有特殊考虑的,这样做是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的意思,这种情况就可以把索引值置为0来表示。Class文件结构中只有常量池的容量计数是从1开始的,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。
第9和第10个字节是常量池计数值,因为常量池中常量不固定,所以在常量池入口放置一项u2类型的数据
常量池中的每一项常量都是一个表,共有11种结构“各不相同的表结构数据,这11种表都有一个共同的特点,就是表开始的第一位是一个ul类型的标志位(tag,取值为1至12,缺少标志为2的数据类型),代表当前这个常量属于哪种常量类型,11种常量类型所代表的具体含义如下所示
看图6-3,常量池的第1个常量是07,从表6-1查表可知道该常量类型为CONSTANT_Class_info
,表示类或接口的符号引用。
Oracle公司已经为我们准备好一个专门用于分析Class文件字节码的工具:javap
javap -verbose [class文件名]
举个例子,可以看到一共18个常量全部显示
C:\>javap -verbose TestClass
public class com.monk.TestClass
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // com/monk/TestClass
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/monk/TestClass.m:I
#3 = Class #17 // com/monk/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/monk/TestClass
#18 = Utf8 java/lang/Object
这里我们将11中常量项的结构定义总结成下表
2.3 访问标志
在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或接口层次的访问信息,包括:这个Clss是类还是接口:是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final,等等。具体的标志位及标志的含义见下表
2.4 类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合
例如上面使用javap
命令生成的类信息中找出对应的类和父类的常量
#3 = Class #17 // com/monk/TestClass
#4 = Class #18 // java/lang/Object
2.5 字段表集合
字段表field info
用于描述接口或类中声明的变量。字段(field)包括了类级变量或实例级变量,但不包括在方法内部声明的变量。我们可以想一想在Java中描述一个字段可以包含什么信息?可以包括的信息有:字段的作用域(public、private、protected修饰符)、是类级变量还是实例级变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。下表中列出了字段表的最终格式。
这里也有access_flags
,与前面提到的访问标志类似
name_index
和descriptor_index
:字段的简单名称和描述符
名称很好识别,比如已/
符号出现的一些名称,比较复杂的是描述符,下面我们看看描述符。描述符的
作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L
加对象的全限定名来表示,详见下表
对于数组类型,每一维度将使用一个前置的[
字符来描述,如一个定义为java,lang.String
类型的二维数组,将被记录为:[[Ljava/lang/String;
,一个整型数组int[]
将被记录为[I
。
对于方法来说,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号()
中,比如方法void inc()
的描述符为()V
,方法 java.lang.String toString()的描述符()Ljava/lang/String;
比如第8个常量和第12个常量的描述符表示:
#8 = Utf8 ()V // 表示空参空返回
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I // 表示空参返回Integer