类文件结构
class类文件的结构
1.class文件是一组以8位字节为基础单位的二进制流,各个数据项目按顺序紧凑的排列没有分隔符,当遇到占用8位以上的空间的数据时,则按照高位在前的方式分割成若干个8位字节进行存储。
2.class文件格式采用类似于c语言结构体的伪结构来存储数据,伪结构只有两种数据类型:无符号和表。
无符号数属于基本的数据类型,u1,u2,u4,u8代表1,2,4,8个字节的无符号数,可以用来描述数字、索引引用,数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据构成的复合数据类型,习惯性以“_info”结尾。整个class文件本质就是一张表。
无论是无符号数还是表,当需要描述同一类型数量不定的多个数据时,经常会使用一个前置的容器计数器加若干连续数据项的形式,表示连续的某一类型的数据的集合。
魔数与class文件版本
1.每个class文件的头4个字节称为魔数(Magic Number),作用是确定这个文件是否为一个能被虚拟机接受的class文件。
class文件的魔数值为:0xCAFEBABE。
2.紧接着魔数的4个字节存储的是class的版本好,5、6字节是次版本号,7、8字节是主版本号
常量池
1.在版本号之后是常量池入口,常量池是class文件结构中与其他项目关联最多的数据类型,常量池入口需要放置一项u2类型的数据,代表常量池容量计数值。
2.常量池中主要存放两大类常量:字面量和符号引用。
字面量比较接近于java语言层面的常量概念。
符号引用则属于编译原理方面的概念,包括下面三类常量:
1.类和接口的全限定名
2.字段的名称和描述符
3.方法的名称和描述符
常量池中每一项常量都是一个表(1.7之前有11中表结构,1.7之后有14种表结构),这些表开始的第一位是一个u1类型的标志位,代表当前的常量属于哪种常量类型。
constant_class_info型常量的结构
tag是标志位,区分常量类型。name_index是一个索引值,代表这个类或者接口的全限定名,指向一个constant_Utf8_info类型常量。
constant_Utf8_info常量结构
length值说明了这个UTF-8编码的字符串长度是多少,后面跟的是长度为length字节的连续数据,该数据使用utf-8编码。
index指向一个utf8的索引
访问标志
常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这些标志用于识别一些类或者接口层次的访问信息。
主要包括:
- 是否final
- 是否public,否则是private
- 是否是接口
- 是否可用invokespecial字节码指令
- 是否是abstact
- 是否是注解
- 是否是枚举
access_flags一共有16个标志可以使用,只定义了8个。
类索引、父类索引与接口索引集合
类索引和父类索引都是一个u2(constant_class_info常量)类型的数据,而接口索引集合是一组u2类型的数据集合,class文件中由这三项数据来确定这个类的继承关系。
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,接口索引用于描述这个类实现哪些接口。
对于接口索引集合,入口第一项为u2类型的数据是接口计数器。
字段表集合
字段表集合:用于描述接口或者类中声明的变量,字段包括类级变量和实例级变量,但不包括在方法内部声明的局部变量。
(入口有一个u2类型的数据代表有多少个字段)
所谓的类变量就是静态变量,这个变量不属于类的任何实例。
实例变量是与类的实例相关联的,需要定义类实例才能使用。
java在描述一个字段可以包含什么信息:字段的作用域(public、private和protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、是否可被序列化(transient修饰符)、字段的数据类型(基本类型、对象、数组)以及字段名称。这些信息中,各个修饰符都是布尔值表示,要么有某个修饰符,要么没有,而字段叫什么名字、字段被定义为什么类型数据都是无法固定的,只能用常量池中的常量来表示。下面是字段表的格式:
(包括访问标注、字段简单名索引、字段和方法的描述符索引、属性表结合)
这里面的access_flags(标志位)与类中的access_flags项目是非常类似的,都是一个u2数据类型,含义如下
跟随者access_flags的是两项索引值:name_index和descriptor_index,它都是对常量池的引用,前者代表字段的简单名称,后者代表字段和方法的描述符。
(简单名称:没有类型和参数修饰的方法或者字段名称
字段和方法描述符:是用来描述字段的数据类型、方法的参数列表和返回值。
根据描述符规则,基本数据类型和无返回值的void的类型用一个大写字母表示,对象则用字符L加对象全限定名来表示
)
descriotor_index之后跟随着一个属性表集合用于存储一些额外信息。
(额外信息
)
字段表集合中不会列出从父类或接口中继承来的字段,但有可能会出现原本Java程序中没有的字段。比较典型的例子是内部类,为了在内部类中保持对外部类的访问性,会增加一个指向外部类实例的字段。另外,在Java语言中字段无法重载,也就是字段名不能重复,即使两个字段的数据类型、修饰符都不相同。不过对于字节码来说,如果两个字段的描述符不一致,那么就可以有重复的字段名
方法表集合:
class文件中对方法的描述和字段描述几乎采用完全一致的方式。结构包括访问标注、名称索引、描述符索引、属性表集合。
(入口有一个u2类型的数据代表有多少个方法)
属性表集合
字段表、方法表都有自己的属性表集合。
属性表集合存在的位置也是不确定的,不仅可以存储在class文件结尾处,还可以作为数据项存在于类、方法表集合和字段表集合中。
1.存在于类中的属性表集合,存储了关于这个类的一些信息。
这部分太过复杂不详细列举。
字节码指令简介
java虚拟机的指令由一个字节长度、代表着某种特定操作含义的数字(操作码)以及跟随其后的零至多个代表此操作所需要参数(操作数)而构成。
字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构,由于限制了 Java 虚拟机操作码的长度为一个字节(即 0 ~ 255),这意味着指令集的操作码总数不可能超过 256 条;又由于 Class 文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时从字节中重建出具体数据的结构,如果要将一个 16 位长度的无符号整数使用两个无符号字节存储起来(将它们命名为 byte1 和 byte2),那它们的值应该是这样的:
这种操作在某种程度上会导致解释执行字节码时损失一些性能。但这样做的优势也非常明显,放弃了操作数长度对齐(注:字节码指令流基本上都是单字节对齐的,只有 “tableswitch” 和 “lookupswitch” 两条指令例外,由于它们的操作数比较特殊,是以 4 字节为界划分开的,所以这两条指令也需要预留出相应的空位进行填充来实现对齐),就意味着可以省略很多填充和间隔符号;用一个字节来代表操作码,也是为了尽可能获得短小精干的编译代码。这种追求尽可能小数据量、高传输效率的设计是由 Java 语言设计之初面向网络、智能家电的技术背景所决定的,并一直沿用至今。
字节码和数据类型
在java虚拟机的指令集中,大多数指令都包含了其操作所对应的数据类型信息。
由于java虚拟机的操作码长度只有一个字节,所以包含数据类型的操作码就为指令集的设计带来了很大的压力。所以java只有限类型相关指令去支持他,并非每一种数据类型和每一种操作都有对应的指令,一些单独的指令可以在必要的时候用来将不支持的类型转换为可被支持的类型。
9类字节码操作
加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。
运算指令
运算指令:用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
类型转换指令
类型转换指令:可以将两种不同的数据类型进行相互转换,一般用户实现代码中显示类型转换操作。
对象创建与访问指令
对象创建与访问指令:
操作数栈管理指令
直接操作操作数的指令
控制转移指令
可以让java虚拟机有条件或无条件地从指定的位置指令继续执行
方法调用和返回指令
异常处理指令:
同步指令
可以支持方法级的同步和方法内部一段指令序列的同步。
公有设计和私有实现
java虚拟机规范描述了java虚拟机应有的共同程序存储格式:class文件格式以及字节码指令集。这些内容与硬件、操作系统以及具体的java虚拟机实现之间是完全独立的,具体实现方式是实现着自己的事,只要外部接口与规范描述一致即可。
虚拟机实现的方式主要有一下两种:
1.将输入的java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集。
2.将输入的java虚拟机代码在加载或者执行时翻译成宿主机cpu的本地指令集。