8.1 Java字节代码格式

大多数开发人员对Java字节代码的格式可能会有点陌生。字节代码一般出现在Java源代码编译之后生成的class文件中。每个class文件中包含了单个类或接口的定义。Java源文件中的内部类会被编译到单独的class文件中。实际上,字节代码并不是只存在于class文件中,还可以通过网络从远程服务器下载,或者由程序在运行时动态生成。所以,字节代码更加准确的说法是包含单个Java类或接口定义的字节流,通常用byte[]来表示。

Java字节代码是一种二进制格式,其具体的格式在Java虚拟机规范中定义。使用二进制编辑器打开一个class文件,可以看到字节代码的内容。要理解字节代码格式,可以参考对应的Java源代码的组织结构。一个Java类从源代码的角度来说,包含类本身的信息及类中包含的域和方法的信息。字节代码中也包含同样的信息,并且以松散的结构进行组织。为了节省空间,字节代码对Java类中常量的存储进行了优化。了解字节代码的格式,是对字节代码进行操作的基础。工具无法为开发人员屏蔽与字节代码相关的所有细节。为了避免涉及过多的细节,本节只对重要的内容进行介绍。在介绍字节代码格式的时候,采用的是Java虚拟机规范中的描述方式。

在介绍字节代码的格式之前,先说明一下Java中的类或接口、域和方法等在字节代码中的表现形式。在Java源代码中引用一个类或接口时,使用的是类似“com.java7book.chapter8.Sample”这种形式的包含名称空间的全名。通过使用import语句,可以省略类或接口所在的包的名称。而在字节代码中,始终使用全名,并且把全名中的“.”替换成“/”,即类似“com/java7book/chapter8/Sample”这样的形式。对于域和方法来说,在字节代码中使用描述符来说明其类型。对于域来说,其类型可能是Java的基本类型、对象类型或数组类型。基本类型在字节代码中用一个字符来表示:byte、char、double、float、int、long、short和boolean类型对应的字符分别是B、C、D、F、I、J、S和Z。对象类型的表示方式是在全名上加“L”前缀和“;”后缀。例如,一个String类型的域的描述符是“Ljava/lang/String;”。数组类型的表示形式是在其元素类型之前加上“[”作为前缀。“[”的个数表示数组的维度。例如,一个double类型的二维数组的描述符是“[[D”。对于一个方法来说,它的描述符取决于参数和返回值的类型,基本形式是“(参数类型)返回值类型”,参数和返回值类型的表示方式与域相同。如果返回值是void,则用“V”表示。例如,方法声明“int calculate(String str)”的类型描述符是“(Ljava/lang/String;)I”。除了类型描述符之外,类、域和方法还可能包含类型签名信息。类型签名是在Java SE 5.0中随着泛型的加入而被添加到字节代码中的,其目的是在运行时也能通过反射API获取泛型相关的信息。第12章会对泛型进行详细介绍。从前面的介绍中可以看出,字节代码中对各种名称的表示方式有些奇怪,这主要是历史原因造成的。

8.1.1 基本格式

字节代码是一个连续的字节流,其中每个部分所表示的含义是不同的。在进行解析时,需要识别出表示不同内容的字节数据之间的边界。这些数据内容可以分成定长和不定长两类。对于定长的内容来说,只需要根据长度依次读取即可;对于不定长的内容来说,会在数据的最前面给出其长度,以进行读取。为了方便介绍,定长数据的类型用u1、u2和u4等来表示,分别表示1字节、2字节和4字节。多字节的字节顺序采用大端表示。字节代码中数据的整体分布如代码清单8-1所示,其中cp_info、field_info、method_info和attribute_info是表示常量池中常量、域、方法和属性的子结构,有自己内部的格式。

代码清单8-1 字节代码的基本格式


u4 魔法数

u2 小版本号

u2 大版本号

u2 常量池中常量的个数再加1

cp_info 常量池内容的数组

u2 访问控制标记和属性修饰符

u2 当前类或接口信息的常量池序号

u2 父类或父接口信息的常量池序号

u2 实现接口的个数

u2 实现接口名称的常量池序号

u2 域的个数

field_info 包含域信息的数组

u2 方法的个数

method_info 包含方法信息的数组

u2 属性的个数

attribute_info 包含属性信息的数组


下面对代码清单8-1中的内容进行详细介绍。字节代码的前4字节是魔法数(magic number),用作字节代码格式的标识符。魔法数的值固定为0xCAFEBABE,英文含义为“咖啡宝贝”,正好与Java名称的来源相对应。不少二进制格式在起始位置都使用类似这样的标识符。[1]魔法数的作用是可以快速判断一个字节序列是否为合法的字节代码。

紧接着的4字节表示的是字节代码的版本号。前2字节表示小版本号,后2字节表示大版本号。由JDK 7编译器生成的字节代码的版本号是51.0,对应的4字节的值是0x00000033。Java 6和Java SE 5.0对应的字节代码的版本号分别是50.0和49.0。每个虚拟机有固定的所支持的字节代码的版本范围。当虚拟机运行它所不支持的版本的字节代码时,会抛出java.lang.UnsupportedClassVersionError错误。如果不能从其他途径得到字节代码的版本,可以通过查看字节代码的第5到第8字节的值来确定。

接下来的2字节表示常量池(constant pool)中常量的个数再加1。常量池中所包含的是Java中基本类型和字符串常量值、类和接口的名称及域的名称等。每个常量的类型和所占用的字节数是不同的。这些常量被字节代码中的其他部分所引用。相同的常量在常量池中只会出现一次。可以将常量池看成是常量的一个查找表。在引用的时候,只需要指定常量在常量池中的序号即可。在常量池的个数之后,紧接着是由cp_info结构表示的每个常量的具体定义。

接下来的2字节表示的是类或接口的访问控制标记和属性修饰符。每个标记或修饰符对应一个比特位。只需要检查这2字节中特定的比特位是否为1,就可以判断标记和修饰符是否生效。对这两个字节使用比特位与操作可以进行快速判断。其中常见的标记与修饰符包括:ACC_PUBLIC对应于public声明,值为0x0001;ACC_FINAL对应于final声明,值为0x0010;ACC_INTERFACE表明是接口而不是一般类,值为0x0200;ACC_ABSTRACT对应于abstract声明,值为0x4000;ACC_SYNTHETIC声明由编译器生成而不在源代码中出现,值为0x1000;ACC_ANNOTATION声明是一个注解类型,值为0x2000;ACC_ENUM声明是一个枚举类型,值为0x4000。这些标记和修饰符的设置需要遵循Java语言的规范。比如,在Java中,不能将一个接口声明为final。因此在字节代码中,ACC_INTERFACE和ACC_FINAL这两个标记或修饰符不能同时被设置。

接下来的2字节表示的是当前Java接口或类的信息在常量池中的序号。同样,接下来的2字节表示的是当前Java接口或类的父类或父接口信息在常量池中的序号。如果当前类是java.lang.Object,则这两个字节的值为0,因为Object类是唯一没有父类的Java类。如果字节代码表示的是接口,那么对应的父类只能是Object类。

接下来的字节代码的格式就比较简单了,依次表示的是当前接口或类所继承或实现的接口的信息,以及包含的域、方法和属性的信息。由于实现的接口、域、方法和属性都可能存在多个,因此,在字节代码中表示时,先用2字节表示元素个数,紧接着是包含所有元素信息的数组。对于实现的接口的数组,其中的每个元素都是常量池中的序号;而对于域、方法和属性来说,每个元素有自己独特的结构,即代码清单8-1中的field_info、method_info和attribute_info结构。

[1]ZIP格式文件的起始两个字节的值为“PK”,是该格式的发明者Phil Katz姓名的首字母缩写。