字节码文件结构

目录

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文件格式如下图。这张表的结构,不论是顺序、数量、字节长度,都是被严格限定的,全部不允许改变

字节码文件结构_第1张图片

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文件(以十六进制查看)

字节码文件结构_第2张图片

字节码文件:

字节码文件结构_第3张图片

可以看到,前四个字节是魔数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)等。

字节码文件结构_第4张图片

字节码文件结构_第5张图片

比如这个字节码文件,常量池的第一个常量,标志位是0x0a,十进制为10,对应的常量类型是CONSTANT_Methodref_info 

字节码文件结构_第6张图片

4、javap

在JDK的bin目录中,Oracle提供了一个分析字节码文件的工具:javap,添加-v参数,可以输出字节码内容。

来看常量池部分:

字节码文件结构_第7张图片

可以看到,第一个常量确实是Methodref类型,和我手工分析的一样。

此外,还出现了很多代码中没有的常量,比如I、V、、LineNumberTable等。这些是编译器自动生成的,会被其他内容所引用。

它们用来描述一些不便于使用固定字节表达的内容,比如方法的返回值、参数个数以及参数类型等。因为Java中类的个数是无穷尽的,不能使用无符号数来表示每个类,只能通过常量表中的符号引用进行表示。

6、访问标志

常量池结束后,紧挨着的两个字节代表访问标志(access_flags),用于识别一些类或者接口层次的访问信息,包括:

  • 这个Class是类还是接口
  • 是否为public修饰
  • 是否为abstract修饰
  • 等等

字节码文件结构_第8张图片

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类型的常量中的全限定名字符串。

字节码文件结构_第9张图片

接口索引集合,入口处为一个u2类型的接口技术企,表示表的容量。(如果没有实现任何接口,计数器为0,表不占用任何字节)

8、字段表集合

字段表(field_info)用于描述类或接口中声明的变量。

Java中的字段(Field)是指成员变量,包括静态属性(类级)和非静态属性(对象级),但不包括方法内部声明的局部变量。

一个字段可以包括这些信息:

  • 访问权限修饰符(public、default、private、protected)
  • 是否为static静态的
  • 是否为final
  • 并发可见性(是否为volatile,强制从内存读写)
  • 可否被序列化(transient)
  • 字段的数据类型(基本类型、引用类型)
  • 字段名称

要描述整个字段:

  • 有些适合使用标志位(比如修饰符,总数是有限的,可以被枚举)
  • 而有些信息,比如数据类型和字段名,无法被枚举,只能应用常量池的常量来描述

最终,字段表设计成了这样:

字节码文件结构_第10张图片

  • access_flags表示字段的修饰符,具体含义如下:字节码文件结构_第11张图片

描述符的扩展

对于数组类型,每一维度会使用一个前置的“[”。

比如“

  • 一个String[][]二维数组,被记录成 [[Ljava/lang/String; (全限定名后面会跟一个分号,表示结束)
  • 一个int[]一维数组,被记录成 [I

描述符描述方法时,按照“先参数列表,后返回值”的顺序来描述。(因为修饰符已经用标志位表示过了,方法名也引用过,所以只剩下返回类型和参数列表需要表示)

参数列表按照参数的从左到右顺序,放在一组小括号内。

比如这个方法:

字节码文件结构_第12张图片

描述符:()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语句不是由字节码指令完成的,而是采用异常表完成的。
  • 同步指令

你可能感兴趣的:(深入理解Java虚拟机,jvm,java,开发语言)