目录
1、概述
2、JVM的两个无关性
3、Class字节码文件的结构
1、基本存储单位
2、字节码文件数据结构
3、Class文件格式
4、魔数与Class文件的版本
5、常量池
6、访问标志
7、类索引、父类索引与接口索引集合
8、字段表集合
9、方法表集合
10、属性表集合
11、总结
4、字节码指令
1、概述
2、字节码指令集的特点
3、字节码与数据类型
4、指令集包含哪些指令
1、概述
计算机只能执行机器码(机器指令,二进制的0和1),所以写的代码需要被编译器编译成机器码才能被计算机执行。
但虚拟机的蓬勃发展提供了第二种选择,即可以选择把代码编译成“与操作系统和机器指令集无关的,针对虚拟机平台的格式”,再交由虚拟机去执行。
2、JVM的两个无关性
有很多种硬件指令集,也有很多种操作系统。要实现一个程序可以在任何操作系统和硬件组成的平台上运行,就必须在操作系统之上实现。
即设计一种所有平台都支持的程序存储格式:字节码,再为每个平台设计一个规格相同的虚拟机,就可以实现屏蔽差异。
实际上,JVM有两种无关性:
- 平台无关性
- 语言无关性:任何语言编译成的class字节码文件,只要符合JVM规范,都可以在JVM上运行。
实现无关性,虚拟机、字节码格式,二者缺一不可
。
3、Class字节码文件的结构
1、基本存储单位
Class文件是一组以8个字节为基础单位的二进制流
,各个数据项目顺序、紧密地排列,没有任何分隔符,使得整个Class文件的内容全部都是程序运行的必要数据。
如果遇到需要占用8个字节以上空间的数据项,按照高位在前的方式,将它分割成若干个 8个字节 进行存储。
2、字节码文件数据结构
无符号数和表
Class文件格式的数据结构,只有两种数据类型:无符号数、表
- 无符号数:
- 基本数据类型
- u1、u2、u4、u8表示1个字节、2个字节、4个字节、8个字节的无符号数(注意1个字节 = 2个十六进制数)
- 可以用来表示数字、索引引用、数量值或按照UTF-8构成字符串值
- 表:
- 由多个无符号数或其他表构成的复合数据类型
- 常用_info结尾
- 用于描述有层次关系的复合结构的数据
- 整个Class文件本质上也可以看作一张表
集合
无论是无符号数还是表,当需要描述同一类型但数量不确定的多个数据,需要使用一个前置的容量计数器 + 若干个连续数据项的形式,这种形式的数据称为“集合”。
3、Class文件格式
Class文件格式如下图。这张表的结构,不论是顺序、数量、字节长度,都是被严格限定的,全部不允许改变
。
4、魔数与Class文件的版本
魔数
每个Class文件的开头4个字节,被称为“魔数(Magic Number)”。
魔数的唯一作用是,确定这个文件是否是一个可以被虚拟机接受的Class文件
。
Class文件的魔数值为“0xCAFEBABE”。
为什么需要魔数
不只是Class文件,很多文件格式标准都使用魔数来进行身份识别
,因为它比文件扩展名更可靠。
文件格式的制定者可以随便选一个内容作为魔数的值,只要没有和其他格式撞车。
Class文件的版本号
魔数后面的4个字节,存储的是这个Class文件的版本号:
- 第5、6个字节是次版本号
- 第7、8个字节是主版本号
这个版本号指的是该Class文件对应JDK的版本号,JVM拒绝执行超过其要求版本号的Class文件,但能向下兼容。
例如JDK 1.1 能支持版本号的范围是45.0 ~ 45.65535,而JDK 13可生成的主版本号最大为57.0
主版本号与次版本号
主版本号代表JDK的大版本号,每个版本+1。次版本号在早期被使用,从JDK 1.2后,次版本号全部固定为0。在JDK 12之后,JDK的功能太多,一些新特性需要以“公测”的形式放出,所以副版本号重新被启用。如果使用了这种“技术预览版”的JDK,生成的字节码文件会把次版本号标识为65535,便于JVM分辨。
示例
比如随便打开一个Class文件(以十六进制查看)
字节码文件:
可以看到,前四个字节是魔数cafebabe,第5、6个字节是次版本号:0x0000,第7、8个字节是主版本号:0x0034,十进制的52,这是JDK8的版本号。
5、常量池
1、常量池的容量
在版本号后面的是常量池,它是占用Class文件空间最大的数据项之一,属于表类型。
常量池中的常量数目是不确定的,所以在入口设置了一个u2类型的数据,代表常量池的容量(constant_pool_count),它是从1开始的。这样设计可以把0空出来,作为“表示不引用任何一个常量池项目的含义”之用。
比如上图的字节码文件,第9、10个字节为0x0018,十进制为24,代表常量池中有23个常量,索引范围是1~23
2、常量池的内容
常量池中存放两大类常量:字面量、符号引用。
- 字面量:类似Java中的常量概念,比如字符串、声明为final的属性值
- 符号引用:包括以下几类常量
- 被模块导出或者开放的包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
Java代码在编译成Class文件之后,Class文件中不会保存各个方法、字段最终在内存中的布局信息,即无法得到真实的内存地址,无法直接被虚拟机使用。
当虚拟机进行类加载时,会从常量池获得对应的符号引用,再在类创建或运行时解析、翻译到具体的内存地址中。
3、常量
常量池中的每一个常量都是一个表,起始的第一位是一个u1的标志位(表示常量的类型)
每种类型的常量都有着自己的结构,各不相同,内容包括标志位(tag)、长度(length)、有效值(bytes)等。
比如这个字节码文件,常量池的第一个常量,标志位是0x0a,十进制为10,对应的常量类型是CONSTANT_Methodref_info
4、javap
在JDK的bin目录中,Oracle提供了一个分析字节码文件的工具:javap,添加-v参数,可以输出字节码内容。
来看常量池部分:
可以看到,第一个常量确实是Methodref类型,和我手工分析的一样。
此外,还出现了很多代码中没有的常量,比如I、V、、LineNumberTable等。这些是编译器自动生成的,会被其他内容所引用。
它们用来描述一些不便于使用固定字节表达的内容,比如方法的返回值、参数个数以及参数类型等。因为Java中类的个数是无穷尽的,不能使用无符号数来表示每个类,只能通过常量表中的符号引用进行表示。
6、访问标志
常量池结束后,紧挨着的两个字节代表访问标志(access_flags),用于识别一些类或者接口层次的访问信息,包括:
- 这个Class是类还是接口
- 是否为public修饰
- 是否为abstract修饰
- 等等
access_flags占两个字节,有16个标志位可以使用,但目前只定义了其中9个,没有使用到的一律为0
7、类索引、父类索引与接口索引集合
访问标志之后是类索引(this_class)、父类索引(super_class),接口索引集合(interfaces)。
- 类索引:u2类型,用于确定这个类的全限定名
- 父类索引:u2类型,只有一个(Java是单继承的,除了java.lang.Object之外,所有Java类的父类索引都不为0)
- 接口索引集合:是一组u2类型数据的集合。这个类实现的接口,按代码书写的顺序从左到右排列在这个集合中。
这三项数据可以确定一个类的继承关系。
类索引和父类索引各自指向一个类型为CONSTANT_Class_info的类描述符常量。
通过这个常量中的索引值,可以找到定义在CONSTANT_Utf-8_info类型的常量中的全限定名字符串。
接口索引集合,入口处为一个u2类型的接口技术企,表示表的容量。(如果没有实现任何接口,计数器为0,表不占用任何字节)
8、字段表集合
字段表(field_info)用于描述类或接口中声明的变量。
Java中的字段(Field)是指成员变量,包括静态属性(类级)和非静态属性(对象级),但不包括方法内部声明的局部变量。
一个字段可以包括这些信息:
- 访问权限修饰符(public、default、private、protected)
- 是否为static静态的
- 是否为final
- 并发可见性(是否为volatile,强制从内存读写)
- 可否被序列化(transient)
- 字段的数据类型(基本类型、引用类型)
- 字段名称
要描述整个字段:
- 有些适合使用标志位(比如修饰符,总数是有限的,可以被枚举)
- 而有些信息,比如数据类型和字段名,无法被枚举,只能应用常量池的常量来描述
最终,字段表设计成了这样:
- access_flags表示字段的修饰符,具体含义如下:
描述符的扩展
对于数组类型,每一维度会使用一个前置的“[”。
比如“
- 一个String[][]二维数组,被记录成 [[Ljava/lang/String; (全限定名后面会跟一个分号,表示结束)
- 一个int[]一维数组,被记录成 [I
描述符描述方法时,按照“先参数列表,后返回值”的顺序来描述。(因为修饰符已经用标志位表示过了,方法名也引用过,所以只剩下返回类型和参数列表需要表示)
参数列表按照参数的从左到右顺序,放在一组小括号内。
比如这个方法:
描述符:()Ljava/lang/String;
一个Class文件的字段表中不会列出从父类或父接口继承而来的字段,但可能会出现Java代码中不存在的字段,这是因为编译器做了处理。
比如内部类中为了保持对外部类的访问性,内部类编译后,编译器就会自动添加指向外部类实例的字段。
通过描述符,还可以进行字段的合法性检验。只要重名的两个字段,描述符不完全一样,那就是合法的。
9、方法表集合
Class文件中对方法的描述,和字段的处理方法类似。
方法表也包括这几部分:访问标志、名称索引、描述符索引、属性表集合。
方法体中的代码,存放在方法表中的属性表中名为“Code”的属性中。
一个类的Class文件中,不会包含从父类继承来的方法(只要没有重写或重载),但会出现编译器自动添加的方法,最常见的有两个:
- ():类构造器方法
- ():实例构造器方法
它们是用于进行“前端编译与优化”的
为什么重载不能以返回值不同为依据
Java中,要重载一个方法,除了两个方法的简单名称要相同之外,还要求必须有一个和原先方法不同的“特征签名”(指Java代码中的)。
特征签名有两种:
- Java代码中的特征签名包括:方法名称、参数顺序和参数类型。返回值不包含在这个签名中,所以无法依靠返回值对一个方法进行重载。
- 字节码的特征签名则范围更广,除了包括方法名称、参数顺序和参数类型之外,还包含返回值以及受查异常表。
另一个方面,只要描述符不完全一致的两个方法(比如有相同的名称和特征签名,但返回值不同),是可以合法存在于同一个Class文件中的。
(这种情况无法通过Java的编译,但JVM是可以支持的,语言无关性的体现!)
10、属性表集合
Class文件、字段表、方法表都可以携带自己的属性表集合,来描述一些专用信息。
属性表集合的限制相较于Class文件中的其他数据项目,稍微宽松一些,不要求各个属性表的严格顺序。只要不与官方的属性名重复,自己实现的编译器可以向属性表中插入任何属性,JVM遇到自己不认识的属性会忽略掉。
《Java虚拟机规范》定义了一大堆官方属性。对于每一个属性,它的名称都要从常量池中应用一个CONSTANT_Utf-8_info类型的常量来表示,而属性值是自定义的,只需要通过一个u4长度的属性去说明属性值占用的位数即可。
因此,从JDK最早版本到现在,Class文件的结构几乎没有发生过变化,新特性只需要在属性表中添加新属性就可以实现支持。
11、总结
Class字节码文件是一个二进制文件,可以用16进制打开查看细节。使用javap- v可以更加直观,把每个部分都分割好了
里面包含这个类的全部信息。
4、字节码指令
1、概述
Java虚拟机的指令由1个字节长的操作码
,以及跟随其后的零至多个操作数
(代表此操作的参数)组成。
由于JVM采用的是面向操作数栈的架构,所以大多数指令都不包含操作数,只有一个操作码。指令参数存放在操作数栈中。
2、字节码指令集的特点
字节码指令集的优缺点都很明显。
缺点:
- 由于限制了JVM操作码的长度为1个字节(8位,0~255),所以指令的总数不能超过256条。
- Class文件格式放弃了编译后代码的操作数长度对齐,虚拟机在操作长度超过1字节的数据时,只能在运行时从字节中重建出它的具体数据结构,损失一些性能。
优点:
- Class文件可以省略掉大量的分隔符,获得数据量小的优势,便于在网络上高效率传输。这也是Java的初衷。
3、字节码与数据类型
JVM的指令集,大多数指令都对应着具体的数据类型,不是通用的。
比如:
- iload指令,从局部变量表中加载int类型的数据到操作数栈中
- 同样的操作,加载float类型需要使用fload指令。
但由于指令个数有限,无法为每种数据类型都唯一安排一个专属的操作指令,所以有一些单独的指令可以在必要的时候,将一些不被支持的类型转换为可被支持的类型。比如byte、short、boolean和char。
大多数指令都没有照顾到这四种类型,所以编译器会在编译期或运行期:
- 将byte和short类型“带符号扩展”成int类型
- 将boolean和char“零位扩展”成int类型
使用int类型的指令来处理。所以大多数对于byte、short、boolean和char类型的操作,实际上都是扩展成int类型来进行的。
JVM对int类型的支持非常完善,很多操作都是最终转化成int类型来进行的。
4、指令集包含哪些指令
一笔带过,作为了解
- 加载和存储指令:用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
- 运算指令:对操作数栈上的两个值进行运算(加减乘除、按位运算、自增、比较)
- 类型转换指令:实现显式类型转换(JVM自动支持低向高类型转换,即隐式类型转换),或用来处理数据类型与指令类型无法对应的问题
- 对象创建与访问指令:创建类实例、访问类字段、检查实例类型
- 操作数栈管理指令:直接控制 操作数栈
- 控制转移指令:修改PC寄存器的值,使得可以从指定位置指令的下一条指令继续执行程序。
- 方法调用和返回指令
- 异常处理指令:处理throw操作,以及自动抛出一些运行时异常。catch语句不是由字节码指令完成的,而是采用异常表完成的。
- 同步指令