1:Class文件的结构
1.1:Class类文件的结构
class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
Class文件以采用一种类似C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基本数据类型,已u1、u2、u4、u8来分别代表1,2,4,8个字节,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其它表作为数据项构成的复合数据类型。
Class文件格式ClassFile结构体的C语言描述如下:
struct ClassFile
{
u4 magic; //识别Class文件格式,具体值为0xCAFEBABE,
u2 minor_version; // Class文件格式副版本号,
u2 major_version; // Class文件格式主版本号,
u2 constant_pool_count; // 常数表项个数,
cp_info **constant_pool; // 常数表,又称变长符号表,
u2 access_flags; //Class的声明中使用的修饰符掩码,
u2 this_class; //常数表索引,索引内保存类名或接口名,
u2 super_class; //常数表索引,索引内保存父类名,
u2 interfaces_count; //超接口个数,
u2 *interfaces; //常数表索引,各超接口名称,
u2 fields_count; //类的域个数,
field_info **fields; //域数据,包括属性名称索引,域修饰符掩码等,
u2 methods_count; //方法个数,
method_info **methods; //方法数据,包括方法名称索引,方法修饰符掩码等,
u2 attributes_count; //类附加属性个数,
attribute_info **attributes; //类附加属性数据,包括源文件名等。
};
魔数和class文件的版本
每个Class文件的头4个字节称为魔数,它的作用是确定这个文件是否是一个能被虚拟机接受的Class文件(0xCAFFBABY 咖啡宝贝)。紧接着魔数的4个字节存储的是Class文件的版本号。
常量池
紧接着主次版本号后的是常量池入口,由于常量的数量不是固定,所以需要在常量池的入口处放置一项u2类型的数据,代表常量池容量计算值,与Java语言中的习惯不一样,这个容量计数是从1开始的,设计者将第0项常量值空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下想要表达“不引用任何一个常量池的目的”的含义。
常量池中主要存放两大类常量:字面量(Literal)符号引用(Symbolic Reference)。字面量比较接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括三类常量: 类和接口的全限定名;字段和名称和描述符;方法的名称和描述符。
表:常量池的项目类型类型 标志 描述 CONSTANT_Utf8_info 1 Utf8编码的字符串 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_Methoddref 10 类中方法的引用 CONSTANT_InterfaceMethodref_info 11 接口中方法的引用 CONSTANT_NameAndType_info 12 字段或方法的部分符号引用 CONSTANT_Method_Handle 15 表示方法句柄 CONSTANT_Method_type_info 16 标识方法类型 CONSTANT_InvokeDynamic_info 18 标识一个动态方法调用点
常量池是最繁琐的数据,因为针对每种常量类型都有自己的数据结构(这里不详细记录,针对不同常量的数据结构,可以再另行查询)。
javap工具可以用来分析class文件。
3. 访问标志
在常量池结束后,紧接着的两个字节是访问标志(access_flag),这个标志用来识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为Public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
表:访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为Public类型 |
ACC_FINAL | 0x0010 | 是否声明为Final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用InvokeSpecial字节码指令的新语义,InvokeSpecial指令的语义在JDK1.0.2发生过变化,为了区别这条指令的语义,JDK1.0.2以后编译出来的类的这个标志必须为真 |
ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并不是由用户代码生成的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个美剧类型 |
4. 类索引、父类索引与接口索引集合
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合,class文件中由这三项数据来确定类的继承关系。
类索引和父类索引引用的两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过这个常量中的索引值可以找到定义早 CONSTANT_Utf8_info类型的常量中的全限定名字字符串。
5. 字段表集合
字段表用来描述接口或者类中申明的变量。字段包括类级变量和实例级变量,但不包括方法内部申明的局部变量。可以描述的信息有:字段的作用域(private public protected)、是实例变量还是类变量(static)、可变性(final)、并发可见性( volatile修饰符,是否强制从主内存读写)、可否被序列化( transient)、字段数据类型(基本类型、对象、数组)、字段名称。
表:字段表结构
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor | 1 |
u2 | attributes_count | 1 |
attribute_info | attbutes | attributes_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 |
跟随access_flag标志后面分别是name_index和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。其中,全限定名称和简单名称很好理解,例如:org/test/ThreadTest和方法init()的简单吃init。
方法和字段的描述符就要复杂一些。
表:描述符标识字符含义
标志字符 | 含义 |
---|---|
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型Ljava/lang/object |
[或者[[ | 数组类型 |
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照严格的顺序放在一组()之内
6. 方法表集合
方法表集合基本上和字段表差不多,只在访问标志和属性表集合的可选项中存在差别。
7. 属性表集合
其实就是虚拟机规范预定义的一些属性。Java虚拟机规范第二版中预定义了9种虚拟机应当识别的属性,在javase7规范中,已经存在21种。
1.2:字节码指令的介绍
java虚拟机指令是由一个字节长度的、代表某种特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成。
1.2.1. 加载和存储指令
加载存储指令用来将数据在栈帧中的局部变量表和操作数栈之间来回传输。
1.2.2. 运算指令
运算或算数指令用来对两个 操作数栈行的值进行某种特定的运算,并把结果从新存入操作栈顶。
1.2.3. 类型转换指令
类型转换指令用来将两种不同的数值类型进行相互转换,这些转换一般用来实现代码中的显示类型转换操作,或者从来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
1.2.4. 对象创建与访问指令
不同对象的创建指令。
1.2.5. 操作数栈管理指令
如果普通数据结构堆栈那样,java虚拟机提供了直接操作操作数栈的指令。
1.2.6. 控制转移指令
控制转移指令可以让java虚拟机有条件或者无条件地从指令的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或者无条件特修改寄存器的值。
1.2.7. 方法调用和返回指令
调用各种方法的指令。
1.2.8. 异常处理指令
在Java程序中显示抛出的异常都由athrow指令来实现,除了用throw语句显示抛出异常的情况除外,Java虚拟机还规定了运行时异常会在其他Java虚拟机指令检测到异常时自动抛出。
1.2.9. 同步指令
java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来实现。例如 java代码中的synchronize关键字。