Class 类文件结构
计算机是不能直接运行 java代码的,必须要先运行 java虚拟机,再由java虚拟机运行编译后的 java代码。这个编译后的 java代码,就是本文要介绍的java字节码。计算机截止目前只认识0和1,但虚拟机以及大量建立在虚拟机之上的程序语言如雨后春笋般出现并蓬勃发展,将我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序选择与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。
实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不和特定的语言绑定,它只与 “Class文件” 这种特定的二进制文件格式所关联。Class文件中包含了 Java虚拟机指令集和符号表以及若干其他辅助信息。任何一个功能性语言都可以将自己编译成 Class文件,从而实现“一次编写,到处运行”,如下图所示:
Class 类文件的结构
Class 文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class文件之中,中间没有任何分隔符,这使得整个 Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
Java虚拟机规范中规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础,所以这里先介绍这两个概念。
【1】无符号数:基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8编码构成字符串值。
【2】表:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以 “_info” 结尾。表用于描述有层次关系的复合结构的数据,整个 Class文件本质上就是一张表。根据 Java 虚拟机规范,类文件由单个 ClassFile 结构组成:
1 ClassFile { 2 u4 magic; //Class 文件的标志 3 u2 minor_version;//Class 的小版本号 4 u2 major_version;//Class 的大版本号 5 u2 constant_pool_count;//常量池的数量 6 cp_info constant_pool[constant_pool_count-1];//常量池 7 u2 access_flags;//Class 的访问标记 8 u2 this_class;//当前类 9 u2 super_class;//父类 10 u2 interfaces_count;//接口 11 u2 interfaces[interfaces_count];//一个类可以实现多个接口 12 u2 fields_count;//Class 文件的字段属性 13 field_info fields[fields_count];//一个类会可以有个字段 14 u2 methods_count;//Class 文件的方法数量 15 method_info methods[methods_count];//一个类可以有个多个方法 16 u2 attributes_count;//此类的属性表中的属性数 17 attribute_info attributes[attributes_count];//属性表集合 18 }
下面详细介绍一下 Class 文件结构涉及到的一些组件。Class文件字节码结构组织示意图:
魔数与 Class文件的版本
每一个 Class 文件的前 4个字节称为魔数(Magic),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class文件。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名是可以随意更改的。Class文件的魔数值很有“浪漫气息”,值为 “0xCAFEBABE(咖啡宝贝)” 紧接着魔数的4个字节存储的是 Class文件的版本号:第5和第6个字节存储的是次版本号(Minor Version),第7和第8个字节是主版本号(major Version)。Java的版本号是从 45开始的,JDK的每个大版本发布,主版本号向上加1,高版本的 JDK能向下兼容以前版本的 Class文件,但不能运行以后版本的 Class文件,即使文件格式并未发生任何变化,虚拟机必须拒绝执行超过其版本号的 Class文件。
常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为 Class文件之中的资源仓库,它是 Class文件结构中与其他项目关联最多的数据类型,也是占用 Class文件空间最大的数据项目之一,同时它还是在 Class文件中第一个出现的表类型数据项目。由于常量池的数量是不固定的,所以在常量池的入口需要放置一项 u2类型的数据,代表常量池容量计数值(constant_pool_count)。这个容量计数是从1开始的,如上图,常量池容量为十六进制数 0x0016,即十进制的 22,这就代表常量池中有21项常量,索引范围1~21。
常量池主要存放两大常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:
【1】类和接口的全限定名;
【2】字段的名称和描述符;
【3】方法的名称和描述符;
常量池中每一项常量都是一个表,这14种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型
CONSTANT_Class_info结构比较简单,如下:tag 是标志位,name_index 是一个索引值,它指向常量池中一个CONSANT_Utf8_info 类型常量,此常量代表了这个类(或者接口)的全限定名。
类型 | 名称 | 数量 |
u1 | tag | 1 |
u2 | name_index | 1 |
CONSANT_Utf8_info类型的常量结构如下:length 值说明了这个 UTF-8编码的字符串长度是多少字节,它后面紧跟着的长度为 length字节的连续数据是一个使用 UTF-8缩略编码表示的字符串。
类型 | 名称 | 数量 |
u1 | tag | 1 |
u2 | name_index | 1 |
u1 | bytes | length |
顺便提一下,由于 Class文件中方法、字段等都需要引用 CONSTANT_Utf8_info 型常量来描述名称,所以 CONSTANT_Utf8_info型常量的最大长度也就是 Java中方法、字段名的最大长度。而这里的最大长度就是 length的最大值,即u2类型能表达的最大值65535。所以 Java程序中如果定义了超过 64KB英文字符的变量或方法名,将会无法编译。
.class 文件可以通过 javap -v class类名 指令来看一下其常量池中的信息(javap -v class类名-> temp.txt :将结果输出到 temp.txt 文件)。
访问标志
常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括,这个 Class 是类还是接口;是否定义了 public属性;是否定义了 abstract类型;如果是类的话,是否被声明为 final等。
标志名称 | 标志值 | 含义 |
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 | 标识这是一个枚举 |
比如:下图表示 Enployee 类的 class信息,ACC_PUBLIC、ACC_SUPER 的访问标识在二进制的 Class 中表示为 0x0021;
类索引、父类索引与接口索引集合
类索引和父类索引都是一个 u2类型的数据( Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有 Java 类的父类索引都不为 0),而接口索引集合(interfaces) 是一组 u2类型的数据的集合(一个类可以实现多个接口),Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 implents(如果这个类本身是接口的话则是extends) 后的接口顺序从左到右排列在接口索引集合中。
类索引、父类索引和接口索引集合都按照顺序排列在访问标志之后,类索引和父类索引引用两个 u2类型的索引值表示,它们各自指向一个类型为 CONSTANT_Class_info 的类描述符常量,通过 CONSTANT_Class_info类型的常量中的索引值可以找到定义在 CONSTANT_Utf8_info类型的常量中的全限定名字符串。
字段表集合
字段表(field)用于描述接口或者类中声明的变量,字段包含类级变量以及实例级变量,但不包括在方法内部声明的局部变量。一个字段可以包含:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰)、可见性(final)、并发可见性(volatile 修饰,是否强制从主内存读取)、是否可被序列化(transient 修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述信息中,字段修饰符可以使用布尔类型表示,要么有要么没有。而字段名称是无法固定的,只能引用常量池中的常量来描述。
u2 | fields_count;//Class 文件的字段的个数 |
field_info | fields[fields_count];//一个类会可以有个字段 |
字段的 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 |
很明显,在实际情况中,ACC_PUBLIC/ACC_PRIVATE/ACC_PROTECTED 三个标志最多只能选择其一,ACC_FINAL/ACC_VOLATILE 不能同时选择。接口之中的字段必须有 ACC_PUBLIC/ACC_STATIC/ACC_FINAL标志,这都由 Java本身的语言规则所决定的。
方法表集合
u2 | methods_count;//Class 文件的方法的数量 |
method_info | methods[methods_count];//一个类可以有个多个方法 |
methods_count 表示方法的数量,而 method_info 表示的方法表。Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。
method_info(方法表的) 结构:
标志名称 | 标志值 | 含义 |
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 | 方法是否是由编译器自动产生的 |
方法里的 Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为 “Code” 的属性里面,属性表作为 Class文件格式中最具扩展性的一种数据项目。
属性表集合
u2 | attributes_count;//此类的属性表中的属性数 |
attribute_info | attributes[attributes_count];//属性表集合 |
在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。