Java提出了"一次编写,到处运行"的口号,同一份程序可以在不同的平台上运行,实现语言无关性的基础是虚拟机和字节码存储格式,Java虚拟机不和包括Java在内的任何语言绑定。根据《深入理解Java虚拟机》中的内容,下面以《Java虚拟机规范(第2版)》,对应JDK1.4的Java虚拟机说明类文件的结构。
1、Class类文件结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列,中间没有任何分隔符。
1.1 数据结构和文件格式
Class文件中只有2种数据结构:无符号数和表,无符号数是基本的数据类型,以u1、u2、u4、u8代表1个字节、2个字节、4个字节、8个字节的无符号数,而表是由无符号数和其他表组成的结构,整个Class文件本质上就是一张表。下面是Class文件中的数据项:
类型 | 名称 | 数量 |
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count |
u2 | access_flags | 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 |
attributes_info | attributes | attributes_count |
Class文件中字节长度、代表意义、先后顺序等细节都是严格规定不可改变的。下面具体介绍这些数据项。
1.2 魔数和Class文件版本
Class文件前4个字节的魔数是用于身份识别,确定文件能否被虚拟机接受。Java Class文件的魔数是0xCAFEBABE。
接下来的是Class文件的次版本号和主版本号。次版本号是小数点后面的部分,主版本号是小数点前面的部分。比如JDK1.2支持45.0~46.65535,如果有个Class文件的版本号是45.00,那么次版本号就是0,主版本号是45。
1.3 常量池
constant_pool_count是Class文件常量池中常量的数量,如果这个数值是6,说明常量池中有5个常量,因为第0个是空出来用于表达"不引用任何一个常量池项目"的含义。常量池中主要放2类常量:字面量和符号引用。其中符号引用有下面3类:
1、类和接口的全限定名:把类的全名由"."改为"/"
2、字段的名称和描述符:
3、方法的名称和描述符:
Class文件中字段和方法的符号引用需要经过运行期转换后才能得到真正的内存入口地址,当虚拟机运行时,从常量池获取对应的符号引用,然后在类创建或运行时解析、翻译到具体的内存地址。JDK1.7中一共有下面14种常量:
类型 | 标志 | 描述 |
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandler_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 表示方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
这14种常量的表的第一位都是一个u1类型的标志位,用于表示常量类型。引用书中的Java代码进行分析:
package or.fenixsoft.clazz; public class TestClass { private int m; public int inc(){ return m + 1; } }
对应解析出来的Class文件字节码:
...... Constant pool: const #1 = class #2; //org/fenixsoft/clazz/TestClass const #2 = Asciz org/fenixsoft/clazz/TestClass; const #3 = class #4; //java/lang/Object const #4 = Asciz java/lang/Object const #5 = Asciz m; const #6 = Asciz I; const #7 = Asciz <init>; const #8 = Asciz ()V; const #9 = Asciz Code; const #10 = Method #3.#11; //java/lang/Object."<init>":()V const #11 = NameAndType #7:#8; //"<init>":()V const #12 = Asciz LineNumberTable; const #13 = Asciz LocalVariableTable const #14 = Asciz this; const #15 = Asciz Lorg/fenixsoft/clazz/TestClass;; const #16 = Asciz inc; const #17 = Asciz ()I; const #18 = Field #1.#19; //org/fenixsoft/clazz/Testclass.m:I const #19 = NameAndType #5:#6; //m:I const #20 = Asciz SourceFile; const #21 = Asciz TestClass.java;
下面是对应的常量池结构:
1 CA FE BA BE 00 00 00 32 2 00 16 07 00 02 01 00 1D 3 6F 72 67 2F 66 65 6E 69 4 78 73 6F 66 74 2F 63 6C 5 ......
第二行的07开始就是常量池项目,它的常量结构如下表:
类型 | 名称 | 数量 |
u1 | tag | 1 |
u2 | name_index | 1 |
7对应的是CONSTANT_Class_info,接下来的0002表示值为第二个常量。第二个常量是01,即UTF-8编码的字符串,它的结构为:
类型 | 名称 | 数量 |
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
001D即是29,接下来29个字节组成的字符串即是第二个变量代表的字符串。CONSTANT_Utf8_info的长度类型是u2,代表字段名、方法名不能超过65535,正常也不会取这么长的名字吧。其他常量格式不给出,都是按照这样的方式存储的。
1.4 访问标志
访问标志用于标识类或接口层次的访问信息,16个标志位定义了8个,没有使用到的一致为0:
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否声明为final |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial指令新语义 |
ACC_INTERFACE | 0x0200 | 是否是接口 |
ACC_ABSTRACT | 0x0400 | 是否是abstract类型 |
ACC_SYNTHETIC | 0x1000 | 标识类非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 是否是注解 |
ACC_ENUM | 0x4000 | 是否是枚举 |
1.5 类索引、父类索引、接口索引集合
this_class是类的全限定名,super_class是父类的全限定名,接口集合则是根据implements后面的接口顺序从左到右排序。这几项都指向CONSTANT_Class_info常量。
1.6 字段表集合
字段表用于描述类或接口的变量,方法内部定义的局部变量不包含在内。下面是fields字段表的结构:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
1.6.1 access_flags
字段修饰符放在access_flags中,具体如下表:
标志名称 | 标志值 | 含义 |
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 |
1.6.2 name_index
这个表示字段的简单名称,简单名称是指没有类型和参数修饰的方法或字段名,比如常用的有个toString的简单名称。
1.6.3 descriptor_index
描述符,用于描述字段的数据类型、方法的参数列表和返回值。基本数据类型和代表无返回值的void类型用一个大写字符表示,而对象类型则用字符L加对象的全限定名来表示,如下表:
标识字符 | 含义 |
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型 |
对于数组类型,每一个维度用"["表示,如定义一个"java.lang/String[][]"类型,则是"[[Ljava/lang/String",一个整型数组"int[]",则是"[I"。
1.6.4 attributes
属性表,用来存储一些额外的信息,如果有字段"final static int m = 123",则会存在一个名称为ConstantValue的属性,指向常量123。
1.7 方发表集合
方法表的结构和字段表一致,依次包括access_flags、name_index、descriptor_index、attributes_count、attributes。方法表集合可能会有编译器自动添加的方法,比如类构造器"<clinit>"方法和实例构造器"<init>"方法。
1、类构造器:编译器自动收集类中所有类变量和静态语句块中的语句合并而成,顺序为源代码顺序。
2、实例构造器:编译器自动收集类中所有实例变量和非静态语句块中的语句合并而成。
1.7.1 access_flags
方法表的访问标志与字段有所区别:
标志名称 | 标志值 | 含义 |
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_BRODGE | 0x0040 | 方法是否为由编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为sbstract |
ACC_STRICTFP | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否为编译器自动产生的 |
1.7.2 descriptor_index
描述符先按照参数列表,后返回值的顺序描述,参数列表按照参数顺序方法小括号"()"中,方法void inc(int a,char b)的描述符为"(IC)V",java.lang.String.toString()的,描述符为"()Ljava/lang/String"
1.7.3 attributes
方法里的Java代码会存放在属性表中,在一个"Code"的属性中。
1.8 属性表集合
在Class文件、字段名和方法表中都可以携带自己的属性表集合,用于描述某些场景专有的信息。
1.8.1 Code属性
Code属性就是方法表中存放方法代码的地方。Code属性结构为:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code_length | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
1、attribute_name_index
这个属性项是一个指向CONSTANT_Utf8_info常量的索引,值为"Code",代表属性名称。
2、attribute_length
属性值的长度,等于整个属性表的长度减去6个字节(属性名和属性长度占据的6个字节)。
3、max_stack
代表错作数栈深度的最大值,虚拟机运行时根据这个值分配栈帧中的操作栈深度。
4、max_locals
局部变量表所需的存储空间。单位是Slot。长度不超过32位的数据类型,每个局部变量占用一个Slot,double和long占用2个Slot。局部变量占据Slot的和不等于max_locals,代码执行超出一个局部变量的作用域时,其占据的Slot可以复用。
5、code
用于存储源代码编译生成的字节码指令,指令是u1类型的单字节。有些指令是带有参数的,虚拟机知道如何理解。另外code_length虽然定义 了u4类型的长度,实际虚拟机规范限制了一个方法的字节码不能超过65535条。
6、exception_table
方法的异常处理表,它的结构为:
类型 | 名称 | 数量 |
u2 | start_pc | 1 |
u2 | end_pc | 1 |
u2 | handler_pc | 1 |
u2 | catch_type | 1 |
含义为:如果字节码在第start_pc行到end_pc(不包括end_pc)出现了类型为catch_type或其子类的异常,则转到第handler_pc行继续处理。当catch_type的值为0是,任何异常都要转到handler_pc处理。
1.8.2 Exception属性
这个属性跟Code属性平级,与上面的异常表不同,是方法描述时在throws关键字后面列举的异常。结构为:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exception | 1 |
u2 | exception_index_table | number_of_exception |
exception_index_table表示抛出的受检异常,是一个指向常量池CONSTANT_Class_info型常量的索引。