类文件结构
简介
平台无关性(一次编写,到处运行):运行在各种不同硬件平台和操作系统上的Java虚拟机都在可以载入和执行同一种平台无关的字节码,从而实现程序的“一次编写,到处运行”。
任何一个Class文件都对应着唯一的一个类或者接口的定义信息(但是反过来说,类或者接口并不一定都定义在文件中(譬如类和接口也可以动态生成,直接送入类加载器))。
Class文件是一组以8个字节为基础单位的二进制流,各个项目按照严格的顺序紧凑地排列在文件,中间没有任何分隔符。
Class文件由两种数据结构组成
- 无符号数:无符号数数据基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数。它可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
- 表:表是由多个无符号数或者其他表作为数据项构成的符合数据类型,为了区分一般以”_info“结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以看作是一张表,这张表由下面的数据项按严格顺序排列构成
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DZIabrbU-1589603404170)(C:\Users\韩壮\AppData\Roaming\Typora\typora-user-images\image-20200515110058869.png)]](https://i-blog.csdnimg.cn/blog_migrate/edec03356906640b3c1977aaf6e6debb.png)
之后介绍用到了下面这段TestClass.java的类文件
public class TestClass {
private int m;
public int inr(){
return m + 1;
}
}
魔数
每个Class文件的头4个字节成为魔数(magic number),它的唯一作用时确定该文件是否是一个能被虚拟机接受执行的class文件。 Class文件的魔数是:CAFEBABE(咖啡宝贝)。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xCfEIAI4-1589603404181)(C:\Users\韩壮\AppData\Roaming\Typora\typora-user-images\image-20200515110922425.png)]](https://i-blog.csdnimg.cn/blog_migrate/a69a08758e5894a00eace8c7e0aa8796.png)
版本号
紧接着魔术的4个字节存储的是Class文件的版本号:第5、6个字节存储的是次版本号(Minor Version),第7、8个字节存储的是主版本号(Major Version)。
《Java虚拟机规范》明确要求虚拟机必须拒绝执行超过其版本号的Class文件,也就是Java虚拟机只能向下兼容以前的版本号,不能允许以后版本的Class文件。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WZyZpBg6-1589603404187)(C:\Users\韩壮\AppData\Roaming\Typora\typora-user-images\image-20200515111317708.png)]](https://i-blog.csdnimg.cn/blog_migrate/1b6af90dab9a1fa68433dde6f5b2c707.png)
常量池
紧接着主、次版本号的是常量池入口,常量池可以比作Class文件的资源仓库。
常量池入口处放置了一项u2类型的数据,表示常量池容量计数值。并且计数从1开始,例如十六进制0x0016,即十进制的22,表示常量池中有21项常量(#1~#22)。#0表示不引用常量池中的任何项目。
常量池中有两大类常量
- 字面量:比较接近Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
- 符号引用:
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Full Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
常量池中每一项都是一个表,截至JDK13,共有17种不同类型的常量。它们的共同特点是:表结构起始第一位是个u1类型的标志位(tag,取值是下表的标志位),代表着当前常量属于哪一种常量类型

表中没有列出的为了支持模块化系统的两项为:
- CONSTANT_Module_info:19,表示一个模块
- CONSTANT_Package_info:20,表示一个模块中开放和导出的包
CONSTANT_Class_Info类型:
- tag是标志位,用于区分常量类型,表示这是一个Class_Info类型(固定为7)
- name_index是常量池的索引,它指向常量池中的一个CONSTANT_Utf8_Info类型常量,代表此类(或者接口)的全限定类型。例如name_index的值为0x0002,也就是指向常量池中的第二项常量。

CONSTANT_Utf8_Info类型:
- tag是标志位,用于区分常量类型,表示这是一个Utf8_Info类型(固定为1)
- length指说明这个UTF-8编码的字符串长度有多少字节
- bytes是长度为length字节的,使用UTF-8缩略编码表示的字符串

由于length是u2类型,最大值为65535,所以Java中的英文字符的变量名和方法名最大为64KB,如果超出这个范围编译会报错。其他类型不再一一介绍,它们的结构如下表所示:

使用javap -verbose TestClass命令可以查看并分析类文件,下面为类文件中常量池的部分:

访问标志
在常量池结束之后,紧接着的2个字节表示访问标志(access_flags),这个标识用于识别一些类或者接口的访问信息,包括:这个class是类还是接口;是否为public类;是否为abstract类型;如果是类的话,是否被声明为final。具体含义以及标志值如下表所示:

补充:ACC_MODULE:0x8000,标识这是一个模块
类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个U2类型的数据,而接口索引集合(interfaces)是一组u2类型数据的集合。
类索引用于确定这个类的全限定类名,夫类索引用于确定这个类的父类的全限定类型。他们都用一个u2类型的索引值表示,他们各自指向一个类型为CONSTANT_Class_Info类型的类描述符常量,通过CONSTANT_Class_Info类型可以找到定义在CONSTANT_Utf8_Info类型的常量中的全限定类名。

接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口按照implement关键字(或者extends)后的接口顺序从左到右排列在接口索引集合中
字段表集合
字段表(field_info)用于描述接口或者类中声明的变量,包括类级变量以及实例级变量,但不包括方法内部声明的局部变量。字段便结构如下表所示:

字符修饰符放在access_flags中,是一个u2的数据类型,内容如下表所示

跟随access_flags标志的是两项索引值:字段简单名称索引(name_index,例如inc, m),字段和方法描述符(descriptor_index,例如:()V、[Ljava/lang/String)。(详见书228)。

最后跟随着一个属性表集合,用于存储一些额外的信息,字段可以在属性表中附加描述零至多项额外信息。
方法表集合
方法表的结构和字段表一样,依次包含访问标志(access_flags)、名称索引(name_index)、描述索引(descriptor_index)、属性表集合(attributes)

对于方法表,所有的访问标志位及其取值如下表所示:

属性表集合
Class文件、字段表、方法表都可以携带自己的属性表集合以描述某些场景的专有信息。
具体内容参考《深入理解JVM》 第三版,230页。
字节码指令简介
Java虚拟机的字节码是由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode),以及后面跟随的零至多个代表此操作操作所需的参数(称为操作数,Operand)构成。
由于Java虚拟机操作码的长度为一个字节(即0~ 255), 这意味着指令集的操作码总数不型够超过256条;又由于Class文件格式放弃了编译后代码的操作数长度对齐。
- 坏处是:不得不在运行时从字节中重建出具体数据的结构,导致解释执行字节码时损失一些性能
- 好处是:可以省略大量的填充和间隔符,短小精悍
对于大部分与数据相关的指令,他们的操作码助记符中都含有特殊字符来表明专门为哪一种数据类型服务:i表示int;l表示long;s表示short;b表示byte;c表示char;f表示float;d表示double;a表示reference
加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈(见第2章关于内存区域的介绍)之间来回传输。

其中以尖括号结尾的这些助记符都代表了一组指令,例如iload_<n>代表iload_0、iload_1、iload_2、iload_3这几条指令,这样做的原因是可以省略操作数。
运算指令
算术指令用于对操作数栈上的两个值进行某种特定运算,并把结果从新存入操作数栈

特点:
- 对于byte, short, char, boolean这些数据类型都是先转化为int再进行运算
- 在处理整型数据时,只有除法指令(idiv和ldiv)以及求余指令(irem和lrem)中出现除零时会抛出ArithmeticException,其他任何整数运算都不会抛出异常,包括溢出
- 把浮点转为整数时,直接截断小数,选择一个最接近但不大原指的整数,也就是向下取整
类型转换指令
类型转换指令可以将两种不同类型的数据相互转换。
1、宽化类型转换,即小范围类型转化为大范围的安全转换。这些转换无需显式命令:
- int类型到long, float, double类型
- long类型到float, double类型
- float类型到double类型
2、窄化类型转换,需要显式的指令进行转换,这些命令包括i2b, i2c, i2s, l2i, f2i, d2i, f2l, d2l, d2f。窄化类型转换可能导致结果正负号转变、不同的数量级请求的情况,转化过程很可能会导致数据精度丢失
对象创建和访问指令
下面的指令包括对对象的创建(其中类实例和数组的创建和操作采用了不同的指令),和对对象的相关访问操作:

操作数栈管理指令
如同操作一个普通数据结构中的堆栈一样,Java虚拟机提供了一些用于直接操作操作数栈的指令:

控制转移指令
控制转移指令可以让Java虚拟机有条件或无条件地从指定位置指令(而不是控制转移指令)的下一多指令继续执行程序,从概念模型上理解,可以认为控制指令就是在有条件或无条件地修改PC寄存器的值。控制转移指令包括:

方法调用和返回指令
方法调用(分派、执行过程)的几下指令如下图所示:

异常处理指令
在Java程序中显示抛出异常的操作(throw语句)都由athrow指令完成。处理异常(cache语句)不是由字节码指令来实现的,而是采用异常表实现。
同步指令
Java虚拟机可以支持方法级别的同步和方法内部一段指令序列的同步,两种同步结构都是使用管程(Monitor,更常见的是直接称之为锁)来实现。

本文详细解析了Java类文件的结构,包括平台无关性、字节码格式、常量池、访问标志、字段表、方法表等内容。介绍了魔数、版本号、常量池、字段表集合、方法表集合等关键组成部分,以及字节码指令的种类和功能。
5771

被折叠的 条评论
为什么被折叠?



