类文件结构
6.1 概述
- 在过去是将高级编程语言编译称本地机器码,然后由机器执行,由于机器码依赖于操作系统和机器指令集,所以不能实现“一处编译,到处运行”,后来产生了一种与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。
6.2 无关性
6.2.1 平台无关性
- 虚拟机可以载入一种与平台无关的字节码,实现了“一处编译,到处运行”。各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码是构成平台无关性的基石。
6.2.2 语言无关性
- 实现语言无关性的基石仍然是虚拟机和字节码存储格式,Java虚拟机不和任何变成语言绑定,它只和“Class文件”这种特定二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干的其他辅助信息。Java虚拟机基于安全性考虑还要求在Class文件中使用了许多强制性的语法和结构化约束。
- Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码指令组合而成的。字节码命令提供的语义更强大。
6.3 Class文件的结构
- Class文件内容:
- 是一组以8为字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有任何分隔符。当遇到占有8位字节以上地数据项目,则会按照高位在前地方式依次分割成若干个8位字节进行存储。
- Class文件格式:
- 采用类似于C中的结构体的伪结构体来存储数据,其中有两种数据类型:无符号数和表
- 无符号数:属于基本的数据类型,以u1、u2、u4、u8、分别代表1个字节、2个字节、4个字节和8个字节的无符号数,可以用来描述数字、索引引用、数量值或者是按照utf8编码的字符串值。
- 表:由多个无符号数或者其他的表组成的符合数据类型,以“_info”格式结尾,用以描述具有层次结构的复合数据类型。
- Class文件本质上就是一张表。
- 无符号数和表描述同一类型但数量不定的多个数据时的基本格式:前置的容量计数器 + 若干个连续的数据项。
6.3.1 魔数(magic)与Class文件的版本(minor_version、major_version)
- 魔数:
- 位置:每个Class文件的前4个字节
- 作用:唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件,也就是所谓的身份识别。
- 选取:只要是这个魔数没有被广泛使用,并且不会引起混淆即可选用。
- 优点:类似于gif和jpeg等的文件在开头都会有魔数,主要是用来身份识别,不采用文件扩展名进行身份识别是因为扩展名可以更改,安全性低。
- 版本号(minor_version、major_version)
- 位置:紧接着魔数的后4个字节,其中5、6字节表示次版本号,7、8字节表示主版本号。
- Java版本号是从45开始的,对应JDK1.1,高版本可以向下兼容低版本,但不可以向上兼容更高版本。
- 举例:
- 红色框0xCAFEBABY表示魔数
- 蓝色框0x0000表示次版本号
- 紫色框0x0032表示主版本号,50,对应的就是JDK1.6。
6.3.2 常量池
- 常量池紧接着版本号位置。可以将常量池理解为Class文件的资源仓库,存储着字面量和符号引用。常量池是Class文件中与其他数据项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时还是在Class文件中第一个出现的数据项目。
- 常量池中存储的常量数不是固定的,所以在常量池的入口处放置一个u2类型的数据,代表常量池容量计数值。为了满足后面某些指向常量池的索引值的数据在特定情况下表达不引用任何一个常量池项目,此时将该值置为0,所以该数据的值从1开始。
- 常量池中存储着字面量和符号引用。
- 字面量:如文本字符串,final修饰的常量。
- 符号引用:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- Java代码在进行javac编译的过程中不会“连接”,这是在类加载的过程中动态连接,所以在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段和方法不经过运行时期转换的话无法得到真正的内存入口地址,无法被虚拟机使用。当虚拟机运行时,需要从常量池中获得对应的符号引用,再再类创建或运行时解析、翻译到具体的内存地址之中。
- 常量池中的每一项都是一个表,JDK1.7之前11中,之后又加了3种。这些表的共同特点是在表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量属于哪种常量类型。
- tag是标志位,它的值对应常量池项目表中的常量类型。name_index是索引值,它的值指向常量池中第“值”个常量的标志位,通过此标志位可以在常量池表中找到对应的常量类型。
- tag是标志位,它的值对应常量池项目表中的常量类型。length说明了utf8编码的字符串长度是多少字节,它后面紧跟的长度为length字节的连续数据是一个使用utf8缩略编码表示的字符串。bytes存储数据,数量为length。
- 由于Class文件中的字段和方法都需要使用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度就是Java中方法和字段名的最大长度。u2类型标识的最大长度为65535.所以如果Java程序中定义来了超过64KB英文字符的变量或方法名,将会无法编译。
- 工具:使用javap -verbose 类名 可以分析Class文件,在Class文件中会自动生成java源码中没有的常量,对应字段表、方法表和属性表。
6.3.3 访问标志
- 常量池结束之后紧跟着的两个字节代表的是访问标志(access_flags),用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明final类型等。
- 标志位一共有16个。
6.3.4 类索引、父类索引和接口索引集合
- 类索引和父类索引是u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件用这三项数据来确定这个类的继承关系。
- 类索引用于确定这个类的全限定名。
- 父类索引用于确定这个类的父类的全限定名。Java中类不允许多重继承,所以父类索引只有一个,除了java.lang.Object外,所有的类都有父类,所以父类索引不允许为0。
- 接口类索引集合用于描述这个类实现的接口的集合,这些实现的接口按照implements语句后的接口的顺序排列在接口索引集合中。
- 类索引、父类索引和接口索引集合在访问标志之后,类索引和父类索引使用u2类型的数据标识,各自指向一个类型为CONSTANT_Class_info类描述符常量,通过CONSTANT_Class_info类型中索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。
- 对于接口集合,入口第一项是u2类型的接口计数器,标识索引表的容量。如何该类没有实现任何的接口,那么此计数器为0,后面接口的索引表不占用任何字节。
6.3.5 字段表集合
- 字段表用于描述接口或类中声明的变量。字段包括类级变量以及实例级变量,但不包括定义在方法内部的局部变量。
- 字段可以标识的信息:字段的作用域(public、private、protected)、实例变量还是类变量(static)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、字段数据类型(基本数据类型、对象和数组)、字段名称。
- 字段修饰符放在access_flags项目中,是u2类型。紧跟的是两项索引值:name_index和descriptor_index,它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。
- 方法和字段的简单名称和全限定名:
- 全限定名:例如org/fenixsoft/clazz/test
- 简单名称:例如org.fenixsoft.clazz.test
- 方法和字段的描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。基本数据类型(byte、short、char、boolean、int、long、float、double)以及代表无返回值void类型都用一个大写字符表示,对象类型用字符L加对象的全限定名表示。
- 对于数组类型,每一维度使用一个前置的“[”字符来描述。
- 描述方法时,按照先参数列表,后返回值的顺序描述,参数列表写在“()”中。
- 字段的描述结束,后面的attrubute是紧跟着的属性表,用于存储一些额外的信息。
- 字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能会列出原Java代码中不存在的字段,比如在内部类中为了保证对外部类的访问性,会自动添加指向外部类实例的字段。
- Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名都是合法的。
6.3.6 方法表集合
- 方法表的结构和字段表的结构一样,都是访问标志、名称索引、描述符索引、属性表集合。
- 方法的定义可以通过访问标志、名称索引和描述符索引表达清楚,方法中的代码通过编译器编译成字节码指令后存放在了属性表中的Code属性里面。属性表是方法表中的最具扩展性的数据项目。
- 如果父类方法在子类中没有重写,方法表集合中就不会出现来自父类的方法信息。但有可能会出现由编译器自动添加的方法,最经典的就是实例构造方法和方法。
- 在java语言中,要重载一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是返回值不会包含在特征签名中,Java语言中仅仅靠返回值的不同是不能实现对一个方法重载的。但是在Class文件中,特征签名范围更大,只要描述符不是完全一致的两个方法也可以共存,也就是说如果两个方法具有相同的简单名称和特征签名,只要是返回值不同,也可以共存在Class文件中。
6.3.7 属性表集合
- 在Class文件、字段表和方法表中都可以带有自己的属性表。
- 属性表集合中不会严格要求属性表的顺序,只要不与已有属性表名重复即可,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉不认识的属性。
6.4 字节码指令简介
- Java虚拟机的指令是由一个字节长的、代表某种特定操作含义的数字(称为操作码,Opcode)以及跟随在后面的零至多个代表此操作所需要的参数(称为操作数,Operands)构成,Java虚拟机面向的是操作数栈,所以大多数的指令并不包含操作数,只有一个操作码。
- 字节码指令集是一种指令集架构,由于限制了Java虚拟机的操作码的长度为1个字节,所以指令集的操作码的总数不会超过256个;另外Class文件放弃了编译后代码的对齐特性,所以虚拟机在处理超过一个字节数据的时候,会从字节中重建具体的数据结构。
- Java虚拟机的解释器基本执行模型:
do {
自动计算PC寄存器的值加1;
根据PC寄存器的指示位置,从字节码流中取出操作码;
if(字节码存在操作数) 从字节码流中取出操作数;
执行操作码所定义的操作;
}while(字节码流长度>0)
6.4.1 字节码与数据类型
- 在Java虚拟机中,大多数的指令都包括了其操作所对应的数据类型信息。例如iload代表从局部变量表中的int类型的数据加载到操作数栈中。fload代表从局部变量表中的float类型的数据加载到操作数栈中。着两条指令的操作在虚拟机内部可能会是一条操作来完成的,但在Class文件中它们必须拥有独立的操作码。
- 对应大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表示专门对应哪种数据类型:i表示int类型、l表示long类型、s表示short类型、b表示byte类型、c表示char类型、f表示float类型、d表示double类型、a表示reference类型。
- 大部分的指令都没有支持整数类型byte、char和short,没有任何指令支持boolean。编译器在编译期间或运行期间将byte和short类型的数据带符号扩展为int类型,将char和boolean类型零位扩展为int类型。
6.4.2 加载和存储指令
6.4.3 运算指令
- 用于对两个操作数栈上的值进行某种特定运算,并打结果重新存入到操作数栈顶,
- 类型:
6.4.4 类型转换指令
- 用于将两种不同的数值类型进行相互转换,一般是用于实现用户代码中的显示类型转换操作,或者是处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
- 虚拟机直接支持宽泛类型转换:就是将小范围类型向大范围类型转换。
- 虚拟机实现窄化类型转换必须是显示的用指令来完成。可能会出现上限溢出、下限溢出和精度丢失。
6.4.5 对象创建与访问指令
- 类实例和数组都是对象,在虚拟机中对类实例和数组的创建与操作使用了不同的指令来完成。
- 对象创建后就可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素。
6.4.6 操作数栈管理指令
6.4.7 控制转移指令
- 可以让Java虚拟机有条件或无条件地从指定地位置指令而不是控制转移指令的下一条指令继续执行程序,可认为控制转移指令就是在有条件或无条件地修改PC寄存器地值。
6.4.8 方法调用和返回指令
- 方法调用指令是和数据类型无关地,而方法返回指令是根据返回值地类型区分的。
6.4.9 异常处理指令
- Java程序中显示抛出异常(throw)都是由athrow指令来实现的,虚拟机规范中还规定了运行时异常会在Java虚拟机指令检测到异常状况时自动抛出。
- Java虚拟机中,处理异常(catch语句)不是由字节码指令完成的,而是由异常表实现的。
6.4.10 同步指令
- Java虚拟机支持方法级和语句块的同步,都是使用管程来实现的。
- 方法级同步是隐式的,无须通过字节码指令来控制,它是现在方法调用和返回操作中。
- 方法调用时,虚拟机会检查方法的ACC_SYNCHRONIZED标志位,如果标志位设置了,那么执行线程会首先持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是异常退出)时释放管程。
- 同步一段指令集通常是由Java语言中的synchronized语句块来表示。指令集中的monitorenter和monitorexit两条配对指令来配合synchronized。
- 为了保证monitorenter和monitorexit能配对执行,编译器会自动生成一个异常处理器,这个异常处理器可声明处理所有的异常。