1.Class类文件的结构
任何一个Class文件都对应着唯一一个类或接口的定义信息.Class文件是一组以8位字节为单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符.
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型: 无符号数和表,后面的解析都要以这两种数据类型为基础.
无符号数属于基本的数据类型,以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 | 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 |
attribute_info | attributes | attributes_count |
Class结构不像XML等描述语言, 由于它没有任何的分割符号,所以上表中的数据项,无论是顺序还是数量,甚至是存储的字节序都被严格限定.
2.各项属性意义
- 1.每个Class文件的头4个字节被称为魔数,用来确定这个文件是否为一个能被虚拟机接受的Class文件, 紧接着四个字节存储的是Class文件的版本号, 先是次版本号, 再是主版本号
- 2.常量池:常量池可以理解为Class文件的资源仓库, 是一个表类型的数据项目. 由于常量池的数量是不固定的, 所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量的计数值, 常量池主要存放两大类常量:字面量和符号引用. 字面量接近于Java语言层面的常量概念,如文本字符串,声明为final的常量值等,而符号引用则属于编译原理方面的概念,包括这些常量: 类和接口的全限定名,字段的名称和描述符,方法的名称和描述符. 常量池中的每一项都是表,共14种,但这14中表的开始第一位都是一个u1类型的标志位tag,用于表示常量类型
例如,一个类或接口的符号引用常量为 : CONSTANT_Class_info类型
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
其中第一项tag表示的就是表的类型, name_index是一个索引值,指向常量池中一个CONSTANT Utf8_info类型常量,此常量代表这个类(或者接口)的权限定名
CONSTANT Utf8_info类型:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
length值说明了这个UTF-8编码的字符串长度是多少字节, 后面紧跟着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串
- 3.访问标志(access_flags): 用于识别一些类或者接口层次的访问信息, 包括:这个Class是类还是接口; 是否定义为public类型;如果是类的话是否声明为final等. access_flags一共有16个标志位可以使用, 当前之定义了其中8个,没有使用到的标志位一律是 0
- 4.类索引,父类索引和接口索引集合: 类索引(this_class)和父索引(super_class)都是一个u2型的数据,而接口索引集合(interfaces)是一组 u2类型的数据的集合. Class文件中由这三项数据来确定这个类的继承关系. 类索引 用来确定这个类的全限定名, 父类索引用于确定这个类的父类的全限定名.
- 5.字段表集合: 字段表(field_info)用于描述接口或者类中声明的变量.字段包括(field)包括类级变量以及实例级变量,但不包括在方法内部的局部变量. 每个字段可以包括:字段的作用域(public,private,protected修饰符),是实例变量还是类变量(static修饰符),可变性(final),并发可见性(volatile修饰符,是否强制从主内存读写),可否被序列化(transient修饰符),字段数据类型(基本类型,对象,数组),字段名称, 类型和名称需要通过常量池中的常量来描述
- 6.方法表集合:与字段表类似,不过存储的是类中每个方法的访问标志,名称索引,描述符索引,属性表集合(几项).但方法里的代码呢? 放在了属性表集合中的一个名为"Code"的属性里面. 与字段表集合相对应的, 如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息.
- 7.属性表集合:在Class文件,字段表,方法表都可以携带自己的属性表集合,以用于某些场景专有的信息.例如Code属性,Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内.Code属性出现在方法表的属性集合中
3.字节码指令
Java虚拟机的指令由一个字节长度的, 代表着某种特定操作含义的数字(操作码)以及跟随其后的零至多个代表此操作的所需参数而构成. 由于Java虚拟机采用了面向操作数栈而不是寄存器的架构,所以大多数的指令都不包括操作数,只有一个操作码
如果不考虑异常处理的话,那么Java虚拟机的解释器可以使用 下面的伪代码的执行模型来理解
do{
自动计算PC寄存器的值加1;
根据PC寄存器的指示位置,从字节码流中取出操作码;
if(字节码存在操作数) 从字节码中取出操作数;
执行操作码定义的操作;
} while(字节码流长度 > 0);
编译器会在编译期或运行期将byte和short类型的数据带符号扩展为响应的int类型数据, 将boolean和char类型的数据零位扩展为相应的int类型数据.因此大多数对于boolean,byte,short和char类型的数据的操作,实际上都是使用相应的int类型作为运算类型
1> 加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
- 将一个局部变量加载到操作栈: iload,iload_ <n>,lload,lload_ <n>,fload.fload_ <n>,dload,dload_ <n>,aload.aload_ <n>
- 将一个数值从操作数栈存储到局部变量表:istore,istore_ <n>,lstore,lstore_ <n>,fstore,fstore_ <n>,dstore,dstore_ <n>,astore,astore_ <n>
- 将一个常量加载到操作数栈:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_ <i>,iconst_ <l>,fconst_ <f>,dconst_ <d>
- 扩充局部变量表的访问索引指令 : wide
2.运算指令
运算或算数指令用于对两个操作数栈上的值进行某种特定运算,并将结果重新存入到操作栈顶
3.类型转换指令
类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作.
Java虚拟机直接支持(即转换时无须显式的转换指令)以下数值类型的宽化类型转换:int到long,float或者double类型; long到float,double类型; float到double类型
相对的,处理窄化类型转换时, 必须显式地使用转换指令来完成.
4.对象创建与访问指令
- 创建类实例指令 : new
- 创建数组的指令 : newarray, anewarray, multianewarray
- 访问类字段(static字段, 或称为类变量)和实例字段(非static字段) : getfield, putfield, getstatic, putstatic
- 把一个数组元素加载到操作数栈的指令 : baload, caload, saload, iaload, laload, faload, daload,aaload
- 讲一个操作数栈的值存储到数组元素中的指令 : bastore,castore,sastore,iastore,fastore,dastore,aastore
- 取数组长度的指令 : arraylength
5.操作数栈管理指令
直接用于对操作数栈操作的指令
6.控制转移指令
控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是 控制转移指令的下一条指令继续执行程序,即可以无条件修改PC寄存器的值
7.方法调用和返回指令
- invokevirtual指令用于调用对象的实例方法, 根据对象的实际类型进行分派(虚方法分派)
- invokeinterface指令用于调用接口方法,会在运行时搜索yield实现了这个接口方法的对象,找出适合的方法进行调用
- invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例化方法,私有方法和父类方法
- invokestatic指令用于调用类方法
- invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法, 并执行该方法
8.异常处理指令
9.同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的.
方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作中.
同步一段指令集序列通常是由sychronized语句块来表示的,Java虚拟机的指令集有monitorenter和monitorexit两条指令来支持synchronized关键字的语义