一、概述
在计算机商业领域中,不同的硬件体系结构和不同的操作系统长期并存是必然的结果,所以Java 提出了“Write Once,Run Anywhere ”的口号 。而构成这种平台无关性的基石就是 Java 虚拟机的出现,同时不同平台的虚拟机使用了同一种程序存储格式——字节码。
二、Class类文件的结构
Class文件是一组以8位字节为基础单位的二进制流,各数据项严格按照特定的顺序,特定的结构紧密排列,中间没有任何的分隔符。当遇到需要占用8位字节以上的空间的数据项时,则会按照高位在前的规则分割成若干个8位字节进行存储。
我们可也用二进制查看工具打开编译后的Class文件,如下图:
为了查看方便,用十六进制表示则为:
其对应的java源程序如下:
package per.wuchg;
public class TestClass {
private int m=0;
public int inc(){
return m+1;
}
}
按照Java虚拟机的规范,Class文件采用一种类似C语言的伪结构来存储数据,这种结构会有两种基本的数据类型,无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
表是由无符号数和其他表作为数据项构成的复合数据类型,用于描述有层次关系的复合结构数据。
无论是无符号数还是表,当需要描述同一类型但是数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式。
Class文件本质就是一张表,用Java语言风格的格式表示(来自The Java Virtual Machine Specification, Java SE 8 Edition,下同)如下:
Class文件不会保存字段和方法的最终的内存布局,因为这些字段、方法的符号引用不经过运行期解析的话无法得到真正的内存入口地址,也就无法被虚拟机使用。
接下来以TestClass.Class文件为例(以下简称为 该例)逐个加以说明:
2.1、魔数
魔数的唯一作用就是身份标识,以此来确定该Class文件是否能被虚拟机接受。魔数是u4类型,即占4个字节,其值为0xCAFEBABE,如下图:
2.2、次版本号,主版本号
Java版本号是从45开始的,JDK1.1之后的每个JDK大版本号向上加1。高版本的JDK能向下兼容低版本的Class文件,但不能加载高版本的Class文件,即使文件格式没发生任何变化,虚拟机也拒绝执行超过其版本号的Class文件。
次版本号为:0x0000
主版本号为:0x0034,十进制为 52 , 52-45+1.1=1.8,所以该Class文件是用JDK8编译的,用 java -version 命令查看JDK版本,如下图:
2.3、常量池
常量池中主要存放两大常量: 字面量 和 符号引用。字面量类似Java语法层面的常量,如:文本字符串、声明为final的常量值符号引用属于编译原理方面的概念,包括下面三类常量:
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
描述符的作用是用来描述字段的数据类型、方法的参数列表(参数类型和顺序)和返回值。
描述符标识字符表如下:
图片来自《深入理解Java虚拟机》 周志明 著
对于数组类型,每一维度将使用一个前置的“[”字符来描述,如定义String类型的二维数组和一个整型数组“int[]” 分别记录如下:
[[Ljava/lang/String;
[I
用描述符来描述方法时候,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。
方法 void inc() 的描述符为“()V”;
方法 int inc() 的描述符为 “()I”;
方法 java.lang.String.toString() 的描述符为“()Ljava/lang/String;”;
方法 int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex)的描述符为“([CIICIII)I;”。
常量池中的每一项都是表结构,其中每个表的第一位是u1类型的标志位,代表当前这个常量的类型。
常量池中的项(表)类型和对应的Tag(来自The Java Virtual Machine Specification, Java SE 8 Edition,以下简称 表CT)如下:
由于每个Class文件的常量池中常量的数量不固定的,所以需要在常量池的入口放置一个u2类型的无符号数,代表常量池容量计数[constant_pool_count-1]。
常量池容量索引范围是从1到 constant_pool_count-1
在该例中,紧接着主版本号之后的是 constant_pool_count ,其值为0x0013,用十进制表示为19,即常量池中有18个常量,索引值从1到18,如下图:
接着就是constant_pool,即常量池。
首先,我们来分析第一个常量(每个常量类型的第一位是u1类型的标志位),其Tag值为0x0A,用十进制表示为10,如下图:
通过查找表CT,其对应的类型是CONSTANT_Methodref_info,该类型的结构(来自The Java Virtual Machine Specification, Java SE 8 Edition , 下同) 如下:
class_index 是一个索引值,它指向常量池中一个CONSTANT_Utf8_info 类型常量,此常量代表了这个类(接口)的全限定名称。
class_index 值 0x0004,十进制为4;name_and_type_index 值 0x000F ,十进制为15。我们可以用 javap 命令反编译 TestClass.Class 文件来验证,如下图:
接着是第二个常量,Tag值为0x09 ,十进制为 9,如下图:
通过查找表CT,其对应的类型是CONSTANT_Fieldref_info,该类型的结构为:
class_index 值 0x0003 ,十进制为3;name_and_type_index 值 0x0010 , 十进制为16,如下图:
接着是第三个常量,Tag值为0x07,如下图:
通过查找表CT,其代表CONSTANT_Class_info类型,该类型
的结构如下图:
0x0011 代表name_index,十进制为 17
接着是第四个常量,Tag值为0x07,如下图:
通过查找表CT,其代表CONSTANT_Class_info类型,该类型结构如下图:
name_index 值为0x0012,十进制为18
接着是第五个常量,Tag值为0x01,如下图:
通过查找表CT,其代表CONSTANT_Ctf8_info类型,该类型结构为:
0x0001 代表length,所以该字符串只占一个字节,字符串内容值为 0x6D,查ASCII表可得其值是m,如下图:
.
.
.
.
.
第七个常量,Tag值为0x01,同样为CONSTANT_Utf8_info类型,其length为0x06,字符串内容的对应的ASCII 为
3C 69 6E 69 74 3E
如下图:
依次解析为:
组合后为“”,如下图:
.
.
.
.
.
第十五个常量,Tag值为0x0c,用十进制表示为12,如下图:
通过查找表CT,其对应为CONSTANT_NameAndType_info类型,该类型结构为:
name_index 值为 0x0007
descriptor_index 值为 0x0008
.
.
.
.
.
第十八个常量,Tag为0x01,代表CONSTANT_Utf8_info 类型,由下图可得其length为0x10,即字符串内容有16个字节,如图:
6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74
按照ASCII规则解析其对应的字符为“java/lang/Object”,如下图:
2.4、访问标志
常量池结束后,紧接着的是u2类型的访问标志(access_flags),这个标志用于识别类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否声明为final等。具体的标志位及标志的含义如下:
access_flags 中一共有16个标志位可以使用,当前只定义了8个(以JSR-202规范为准),没有使用到的标志位一律为0。在本例中,TestClass是个普通的Java类,不是接口、枚举或者注解,被 public 关键字修饰但没有声明为 final 和 abstract,因此它的 ACC_PUBLIC 、 ACC_SUPER 为真,其他6个标志位为假,所以该类的access_flags的值为:0x0001|0x0020=0x0021,如下图:
2.5、类索引、父类索引与接口索引集合
类索引、父类索引都是一个u2类型的数据,而且接口索引集合是一组u2类型的数据集合(多重继承),Class文件中由这三项数据来确定这个类的继承关系。
在本例中,该类的类索引和父类索引分别为0x0003、0x0004,如下图:
所以该类索引为 3 ,对应的最终值是 per/wuchg/TestClass;父类索引为 4 ,对应的最终值是 java/lang/Object。
因为接口是索引集合,入口的第一项——u2类型的数据为接口计数器,表示索引表的容量。如果该类型没有实现任何接口,则该计数器值为0,后面的接口的索引表不再占任何字节。
本例中没有实现、继承任何接口,故索引计数器为0x0000,如下图:
2.6、字段表集合
字段表集合用于描述接口或者类中的声明的变量。字段(Field)包括类变量和实例变量,但是不包括方法内部声明的局部变量。紧接着接口计数器的是字段容器计数器,在本例中,其值为0x0001,表示只有1个字段,如下图:
字段表结构如下:
所以 access_flags 的值为0x0002,查找下图可知代表private修饰符的ACC_PRIVATE标志位为真,其他修饰符为假。
name_index 的值为0x0005,descriptor_index 的值为 0x0006,其对应的字符串值分别为“m”和“I” (不知道如何得出值为m和I的同学可以重头看),根据这些信息,我们可以推断出源代码定义的字段应该为:“private int m;”,如下图:
attributes_count 的值为0x0000,即没有额外描述的信息,但是,如果将字段m声明改为“final static int m=0;”那就可能会存在一项名称为ConstantValue的属性,其值指向常量0。
2.7、方法表集合
Class文件存储格式中对方法的描述与对字段的描述几乎采用了相同的描述方式,方法表的结构如图字段表一样,如图:
本例中,方法表集合中,第一个u2类型的数据(即是计数器容量)的值为0x0002,代表集合中有2个方法(这两个方法为编译器添加的实例构造器和源码中的方法inc()),如下图:
第一个方法的访问标志值为0x0001,也就只有ACC_PUBLIC标志为真,如下图:
name_index 值为0x0007,查找常量池得方法名称为“”,描述符索引值为0x0008,对应常量为“()V”,如下图:
attributes_count 值为
attribute_name_index 值为0x0009,对应常量为“Code”,Code属性的结构为:
在本例中,对应的值如下图:
attribute_length 值为0x00000026,用十进制表示为38
max_stack 值为0x0002
max_locals 值为0x0001
code_length 值为0x0000000A
code 值为 0x2A B7 00 01 2A 03 B5 00 02 B1
exception_table_length 值为 0x0000
attributes_count 值为0x0001
根据虚拟机字节码指令表翻译出所对应的字节码过程如下:
读入2A ,查表 0x2A 对应指令为 aload_0 ,这条指令的含义将第0个Slot中为引用类型本地变量推送至操作数栈顶。
读入B7,查表 0xB7 对应指令为 invokespecial ,这条指令的作用是以栈顶的引用类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法,私有方法或者它的父类的方法。这个方法有u2类型的参数说明具体调用哪一个方法,它指想常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的符号引用。
读入00 01 ,这是invokespecial的参数,查常量池的0x0001对应的常量为实例构造器“”方法的符号引用。
读入2A,同第一步。
读入03,查表0x03 对应指令为iconst_0,这条指令的含义将int型0推送至栈顶。
读入B5,查表0xB5 对应指令为putfield , 这条指令的含义是为指定类的实例域赋值。
读入00 02 ,这是 putfield 命令的操作数,查常量池的0x0002对应的常量为实例字段m。
读入B1,查表0xB1对应的指令为return,含义是返回次方法,并且返回值为void。这条指令执行后,当前方法结束。
如下图:
Code 属性中包含一个 attribute_name_index 值为0x000A 的属性,对应的常量为“LineNumberTable”,其结构为:
attribute_length 值为0x0000000A
line_number_table_length 值为0x0002
(start_pc,line_number) 值为(0x0000,0x0006)(0x0004,0x0008)
接下来是二个方法的访问标志值为0x0001,也就只有ACC_PUBLIC标志为真,故第二个方法的修饰符是public,name_index 值为 0x000B ,descriptor_index 值为 0x000C,如下图:
查询常量池可知,
该方法表中有一个属性,attribute_name_index 值为 0x0009,常量值为“Code”。Code属性表中的attribute_length 值为0x0000001F,max_stack 值为 0x0002
max_locals 值为0x0001,code_length 值为0x00000007 ,code 值为
2A B4 00 02 04 60 AC
如下图:
根据虚拟机字节码指令表翻译出所对应的字节码过程如下:
读入2A,查表 0x2A 对应指令为 aload_0 ,这条指令的含义将第0个Slot中为引用类型本地变量推送至操作数栈顶。
读入B4,查表 0xB4 对应指令为 getfield,这条指令的含义是获取指定类的实例域,并将其值压入栈顶。
读入00 02 ,这是 getfield 命令的操作数,查常量池的0x0002对应的常量为实例字段m。
读入04,查表0x04对应指令为iconst_1 ,这条指令将int型1推送至栈顶。
读入60,查表 0x60 对应指令为 iadd,这条指令的含义是将栈顶两int型数值相加并且将结果压入栈顶。
读入AC,查表0xAC 对应指令为ireturn,从当前方法返回int。
如下图:
2.8、属性表集合
属性表与Class文件中其他数据项要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格的顺序,只要求不要与已有属性名重复,其结构如下:
在本例中,0x0001 代表属表集合的容量,0x000D 代表属性名称索引,其常量值为 “SourceFile ”,结构为:
sourcefile_index 值为 0x000E,用十进制表示为14,表示源码文件名称,如图: