1.引言
- 以前编写程序都是编译成二进制本地机器码,然而现在选择了新的文件格式----------Class文件。
- 以前总以为程序的“一次编写,到处运行”指的是java程序,现在才发现指的是所有可以编译存储为Class文件的编程语言,虚拟机可以载入和执行同一种(平台无关)的字节码。
- 以前只知道java的跨平台是虚拟机实现的,现在底层实际是虚拟机先将程序编译成Class文件,然后在不同的平台(所谓的平台指的是不同的操作系统,不同的CPU有不一样的机器指令集)上转换成机器码。
2 Class类文件结构
Class文件以8位字节为基础的二进制流,包含无符号数和表两种数据结构。
无符号数属于基本的数据结构,以u1,u2,u4,u8分别代表1个字节,2个字节,4个字节和8个字节的无符号数,无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。它所包含的数据项如下。
类型 | 名称 | 数量 |
u4 | magic(魔数) | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interface_count | 1 |
u2 | interface | interface_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attribute_count | 1 |
attribute_info | attribute | attributes_count |
我们用简单的java代码待观察Class文件格式。
public class ClassForm {
private int m;
public static void main(String[] args) {
}
public int test() {
return m+1;
}
}
然后用WinHex打开这个Class文件:
2.1魔数
· 每个Class文件的头四个字节是魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。魔数值为:CAFEBABE。
2.2Class文件的版本(minor_version & major_version)
第5个字节和第六个字节代表次版本号,第7个字节和第8个字节代表主版本号。如图所示次版本号是0x0000,而主版本号是0x0032,也就是十进制的52,代表是jdk1.8.JDK1.1是45,依次累加。
2.3常量池(constant_pool)
1.常量池是资源仓库,占用Class文件最大的数据项之一。第9个字节和第10个字节是指常量池里面的常量个数,001A表示26,但是0不表示常量,他在必要的时候需要表达“不引用任何一个常量池项目”的含义,所有只有25个常量。
2.常量池中每一个常量都是一张表,这些表类型共有14中(JDK1.7),按顺序编号,便于标识。大家如果想了解具体的类型可以自行搜索。每张表类型都有它自己独特的表结构,他们的相同点就是表的第1位是一个u1类型(一个字节)的标志位(tag),代表当前这个常量属于哪种常量类型。
3.常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References),字面量类似于Java语言中常量概念,如文本字符串,声明为final的常量值等。而符号引用则包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
接下来的字节代表的就是每个常量对应表数据结构的内容。比如第11和第12位为0x07表示第一个常量的类型属于第7种:CONSTANT_Class_Info(大家可以去搜索)。该表的结构如下:
类型 | 名称 | 数量 |
u1 | tag | 1 |
u2 | name_index | 1 |
所以接下来的两个字节就代表name_index,这里是0x0002表示指向常量池中的第二个常量。我们继续往下看下一个常量:接下来一个常量的tag是0x01表示第一种常量类型:CONTSANT_Utf8_info,它的结构如下所示:
类型 | 名称 | 数量 |
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
length的值说明这个UTF-8编码的字符串是多少字节,这里是0x0009就是指9个字节,bytes就表示接下来的9个字节就表示这个字符串,他所表示的就是ClassForm这个类名(如果有包名还要加上包名)。
2.4访问标志(access_flags)
在常量池访问结束后,紧接着的两个字节代表访问标志,这个标志用于一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。具体的标志位如下:
标志名称 | 标志位 | 含义 |
ACC_PUBLIC | 0X0001 | public |
ACC_FINAL | 0X0010 | final |
ACC_SUPER | 0X0020 | 不是很懂,但是必须为真 |
ACC_INTERFACE | 0X0020 | 接口 |
ACC_ABSTARCT | 0X0400 | 抽象 |
ACC_SYNTHETIC | 0X1000 | 并非用户代码生成的 |
ACC_ANNOTATION | 0X2000 | 注解 |
ACC_ENUM | 0X4000 | 枚举 |
标志位共2个字节,16个标志位,当前只定义其中八个,没有使用到的标志位要求一律为0.在我们写的这段代码中,只有ACC_PUBLIC和ACC_SUPER为真,所以这个标志位应为0x0021.验证如下。
2.5类索引,父类索引与接口索引(this_class, super_class, interfaces)
类索引用于确认这个类的全限定名,父类索引用于确认这个类的父类全限定名,由于java不允许多继承,所以父类索引只有一个,因为所有类都继承于java.lang.Object,所以除了Object之外,所有的java类都有父类索引。实现的接口按照从左到右的顺序排列在接口索引集合中。它们固定用CONSTANT_Class_info常量结构,通过CONSTANT_Class_Info常量中的索引值找到定义在CONSTANT_Utf8_info中的全限定名字符串。
首先是类索引:这里是0x0001表示指向常量1,而常量1我们上面已经分析过了,常量1指向常量2,常量2表示ClassForm这个字符串。
再而是父类索引:这里是0x0003代表指向常量3,而常量3我们可以分析得出是指向常量4,而常量4代表java.lang.Object。
最后是接口索引:这里为0x0000代表没有实现接口。
2.6字段表集合(field_info)
字段表用于描述接口或者类中声明的变量。字段包括类级变量(static)以及实例级变量,但不包括在方法内部声明的局部变量。一个字段包含的信息有很多:如作用域public,private,protected,final,static,volatile,transient,字段基本类型,名称。
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attribute_count | 1 |
attribute_info | attributes | attribute_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 |
name_index:字段的简单名称
descriptor_index:字段或方法的描述符
标识字符 | 含义 | 标识字符 | 含义 |
B | 基本类型为byte | J | 基本类型为long |
C | 基本类型为char | S | 基本类型为short |
D | 基本类型为double | Z | 基本类型为boolean |
F | 基本类型为float | V | void |
I | 基本类型为int | L | 对象类型,如Ljava/lang/Object |
举例:一个整型数组“int[]”将被记录为“[I”.一个String[][]二维数组将被记录为:”[[Ljava/lang/String;“。如果用来描述方法,按照参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号”()“内。如方法 void main(String[] args) 描述为 ([Ljava/lang/String;)V(注意有个分号,全限定名后面都有一个分号)。
attributes_count用来需要额外描述的信息量,比如初始化等。
attributes表就是用来描述额外的信息。
我们可以进行验证:接下来的字节表示的是字段个数fields_count:这里是0x0001,表示只有一个字段。
接下来就是字段表里的第一个access_flags,文件里是0x0002表示private。
第二个是name_index:文件里为0x0005,表示指向第5个常量,而第5个常量表示m。
第三个是descriptor_index:文件里是0x0006,表示指向常量池第六个常量,第六个常量表示”I“,表示 基本类型为int。
后面的attributes_count为0x0000
综上所述,我们可以推断出代码为private int m。
2.7方法表集合(methods)
方法表和字段表几乎采用了相同的存储格式:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attribute_count | 1 |
attribute_info | attributes | attribute_count |
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_BRIDGE | 0X0040 | 字段是否是由编译器产生的桥接方法 |
ACC_VARARGS | 0X0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0X0100 | 方法是否为native |
ACC_ABSTRACT | 0X0400 | 方法是否为抽象 |
ACC_STRICTFP | 0X0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0X1000 | 方法是否是由编译器自动产生 |
我们在对ClASS文件进行验证:
首先的两个字节是方法数methods_count:文件里为0x0003,表示有三个方法。
之后是access_flags:文件里是0x0001,表示第一个方法是public
之后是name_index:文件里是0x0007,指向常量池中的第7个常量,第七个常量池中为 <init>(编译器自动添加)
之后是descripator_index: 文件里是0x0008,指向常量池里的第八个常量,第八个常量为 ()V(上面已经对这个符号进行了描述)
之后是attribute_count:文件里是0x0001,就表示此方法属性表集合有一项属性。
最后是attributes:文件里是0x0009,表示指向常量池中的第9个常量,第9个常量为Code,接下来会讲解Code属性。
2.8属性表集合
在Class文件里,字段表,方法表都可以携带自己的属性表集合。在虚拟机规范中预定义了很多种属性,每个属性都有对用的使用位置和含义。每个属性都是用CONSTANT_Utf8_info类型常量来表示.这里就不一一介绍了这里就介绍方法表里面的Code属性。
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
备注:用户可以自定义info的内容,只需要标明attribute_length长度即可。
类型 | 名称 | 数量 |
u2 | attribute_name_length | 1 |
u2 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u2 | code_length | 1 |
u2 | code | code_length |
u2 | exception_table_length | 1 |
u2 | exception_table | exception_table_length |
u2 | attribute_count | 1 |
u2 | attribute | attributes_count |
max_stack:代表栈帧里操作数栈的深度的最大值。
max_locals代表局部变量所需的存储为slot空间,单位为slot。
code_length和code用来存储Java源程序编译后生成的字节码指令,code_length代表字节码长度。
Code属性是Class文件中最重要的属性,要掌握需要事件,本菜鸟还在学习中。。。。。
3.总结
Class文件格式所具备的平台中立(不依赖硬件及操作系统),紧凑,稳定和可扩展的特点,是java技术体系实现平台无关,语言无关两项特性的重要支柱。同时Class文件是对java虚拟机执行引擎的数据入口,接下来我们需要关注的是字节流在虚拟机执行引擎中是如何被解释执行的。