目录
1、概述
2、JVM的两个无关性
3、Class字节码文件的结构
1、基本存储单位
2、字节码文件数据结构
3、Class文件格式
4、魔数与Class文件的版本
5、常量池
6、访问标志
7、类索引、父类索引与接口索引集合
8、字段表集合
9、方法表集合
10、属性表集合
11、总结
4、字节码指令
1、概述
2、字节码指令集的特点
3、字节码与数据类型
4、指令集包含哪些指令
计算机只能执行机器码(机器指令,二进制的0和1),所以写的代码需要被编译器编译成机器码才能被计算机执行。
但虚拟机的蓬勃发展提供了第二种选择,即可以选择把代码编译成“与操作系统和机器指令集无关的,针对虚拟机平台的格式”,再交由虚拟机去执行。
有很多种硬件指令集,也有很多种操作系统。要实现一个程序可以在任何操作系统和硬件组成的平台上运行,就必须在操作系统之上实现。
即设计一种所有平台都支持的程序存储格式:字节码,再为每个平台设计一个规格相同的虚拟机,就可以实现屏蔽差异。
实际上,JVM有两种无关性:
实现无关性,虚拟机、字节码格式,二者缺一不可
。
Class文件是一组以8个字节为基础单位的二进制流
,各个数据项目顺序、紧密地排列,没有任何分隔符,使得整个Class文件的内容全部都是程序运行的必要数据。
如果遇到需要占用8个字节以上空间的数据项,按照高位在前的方式,将它分割成若干个 8个字节 进行存储。
无符号数和表
Class文件格式的数据结构,只有两种数据类型:无符号数、表
集合
无论是无符号数还是表,当需要描述同一类型但数量不确定的多个数据,需要使用一个前置的容量计数器 + 若干个连续数据项的形式,这种形式的数据称为“集合”。
Class文件格式如下图。这张表的结构,不论是顺序、数量、字节长度,都是被严格限定的,全部不允许改变
。
魔数
每个Class文件的开头4个字节,被称为“魔数(Magic Number)”。
魔数的唯一作用是,确定这个文件是否是一个可以被虚拟机接受的Class文件
。
Class文件的魔数值为“0xCAFEBABE”。
为什么需要魔数
不只是Class文件,很多文件格式标准都使用魔数来进行身份识别
,因为它比文件扩展名更可靠。
文件格式的制定者可以随便选一个内容作为魔数的值,只要没有和其他格式撞车。
Class文件的版本号
魔数后面的4个字节,存储的是这个Class文件的版本号:
这个版本号指的是该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的版本号。
1、常量池的容量
在版本号后面的是常量池,它是占用Class文件空间最大的数据项之一,属于表类型。
常量池中的常量数目是不确定的,所以在入口设置了一个u2类型的数据,代表常量池的容量(constant_pool_count),它是从1开始的。这样设计可以把0空出来,作为“表示不引用任何一个常量池项目的含义”之用。
比如上图的字节码文件,第9、10个字节为0x0018,十进制为24,代表常量池中有23个常量,索引范围是1~23
2、常量池的内容
常量池中存放两大类常量:字面量、符号引用。
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中类的个数是无穷尽的,不能使用无符号数来表示每个类,只能通过常量表中的符号引用进行表示。
常量池结束后,紧挨着的两个字节代表访问标志(access_flags),用于识别一些类或者接口层次的访问信息,包括:
access_flags占两个字节,有16个标志位可以使用,但目前只定义了其中9个,没有使用到的一律为0
访问标志之后是类索引(this_class)、父类索引(super_class),接口索引集合(interfaces)。
这三项数据可以确定一个类的继承关系。
类索引和父类索引各自指向一个类型为CONSTANT_Class_info的类描述符常量。
通过这个常量中的索引值,可以找到定义在CONSTANT_Utf-8_info类型的常量中的全限定名字符串。
接口索引集合,入口处为一个u2类型的接口技术企,表示表的容量。(如果没有实现任何接口,计数器为0,表不占用任何字节)
字段表(field_info)用于描述类或接口中声明的变量。
Java中的字段(Field)是指成员变量,包括静态属性(类级)和非静态属性(对象级),但不包括方法内部声明的局部变量。
一个字段可以包括这些信息:
要描述整个字段:
最终,字段表设计成了这样:
描述符的扩展
对于数组类型,每一维度会使用一个前置的“[”。
比如“
描述符描述方法时,按照“先参数列表,后返回值”的顺序来描述。(因为修饰符已经用标志位表示过了,方法名也引用过,所以只剩下返回类型和参数列表需要表示)
参数列表按照参数的从左到右顺序,放在一组小括号内。
比如这个方法:
描述符:()Ljava/lang/String;
一个Class文件的字段表中不会列出从父类或父接口继承而来的字段,但可能会出现Java代码中不存在的字段,这是因为编译器做了处理。
比如内部类中为了保持对外部类的访问性,内部类编译后,编译器就会自动添加指向外部类实例的字段。
通过描述符,还可以进行字段的合法性检验。只要重名的两个字段,描述符不完全一样,那就是合法的。
Class文件中对方法的描述,和字段的处理方法类似。
方法表也包括这几部分:访问标志、名称索引、描述符索引、属性表集合。
方法体中的代码,存放在方法表中的属性表中名为“Code”的属性中。
一个类的Class文件中,不会包含从父类继承来的方法(只要没有重写或重载),但会出现编译器自动添加的方法,最常见的有两个:
它们是用于进行“前端编译与优化”的
为什么重载不能以返回值不同为依据
Java中,要重载一个方法,除了两个方法的简单名称要相同之外,还要求必须有一个和原先方法不同的“特征签名”(指Java代码中的)。
特征签名有两种:
另一个方面,只要描述符不完全一致的两个方法(比如有相同的名称和特征签名,但返回值不同),是可以合法存在于同一个Class文件中的。
(这种情况无法通过Java的编译,但JVM是可以支持的,语言无关性的体现!)
Class文件、字段表、方法表都可以携带自己的属性表集合,来描述一些专用信息。
属性表集合的限制相较于Class文件中的其他数据项目,稍微宽松一些,不要求各个属性表的严格顺序。只要不与官方的属性名重复,自己实现的编译器可以向属性表中插入任何属性,JVM遇到自己不认识的属性会忽略掉。
《Java虚拟机规范》定义了一大堆官方属性。对于每一个属性,它的名称都要从常量池中应用一个CONSTANT_Utf-8_info类型的常量来表示,而属性值是自定义的,只需要通过一个u4长度的属性去说明属性值占用的位数即可。
因此,从JDK最早版本到现在,Class文件的结构几乎没有发生过变化,新特性只需要在属性表中添加新属性就可以实现支持。
Class字节码文件是一个二进制文件,可以用16进制打开查看细节。使用javap- v可以更加直观,把每个部分都分割好了
里面包含这个类的全部信息。
Java虚拟机的指令由1个字节长的操作码
,以及跟随其后的零至多个操作数
(代表此操作的参数)组成。
由于JVM采用的是面向操作数栈的架构,所以大多数指令都不包含操作数,只有一个操作码。指令参数存放在操作数栈中。
字节码指令集的优缺点都很明显。
缺点:
优点:
JVM的指令集,大多数指令都对应着具体的数据类型,不是通用的。
比如:
但由于指令个数有限,无法为每种数据类型都唯一安排一个专属的操作指令,所以有一些单独的指令可以在必要的时候,将一些不被支持的类型转换为可被支持的类型。比如byte、short、boolean和char。
大多数指令都没有照顾到这四种类型,所以编译器会在编译期或运行期:
使用int类型的指令来处理。所以大多数对于byte、short、boolean和char类型的操作,实际上都是扩展成int类型来进行的。
JVM对int类型的支持非常完善,很多操作都是最终转化成int类型来进行的。
一笔带过,作为了解