JVM的语言无关性

与平台无关性是建立在操作系统上,虚拟机厂商提供了许多可以运行在各种不同平台的虚拟机。它们都可以载入和执行字节码,从而实现程序的”一次编写,到处运行”。

各种不同平台的虚拟机与平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石,也是语言无关性的基础。Java虚拟机不和包括java在内的任何语言绑定,它只与”Class文件”这种特定的二进制文件格式所关联。Class文件中包含了Java虚拟机指集合符号以及若干其他辅助信息。

将class文件批量转成java文件_jvm

Class文件的衍变过程与所处位置

将class文件批量转成java文件_将class文件批量转成java文件_02

Java技术能一直保持非常好的向后兼容性,这点Class文件结构的稳定性功不可没。Java已经发展到14版本,但是class文件结构的内存,绝大部分在JDK1.2的时候就已经定义好了。虽然JDK1.2的内容比较古老,但是java发展经历了十余个大版本,但是每次基本上只是在原本结构基础上新增内容,扩充功能,并未定义的内容做修改。

任何一个Class文件都对应着唯一一个类或者接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在(比如可以动态生成,或者直接送入类加载器中)。Class文件是一组以8位字节为基础单位的二进制流。

Class文件结构这些内容在面试的时候较少,但是身为资深java开发,我们必须去了解它。

Class文件格式

现在我们知道了Class文件存在的意义,以及在整个java运行流程所处的位置了。那么,这个平时我们很少关心的Class文件结构的结构是什么呢?

下面,我们从一段代码入手,看看它对应的的Class文件的"庐山真面目"。

将class文件批量转成java文件_字段_03

首先,先给出一个简单的java程序,并对其进行编译。找到其编译好的Class文件地址。

将class文件批量转成java文件_java_04

将class文件批量转成java文件_将class文件批量转成java文件_05

以上是我们用16进制打开的class文件结构图。文件通过二进制存储,以8个字节为一组,所以以16进制展示。

各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部都是程序运行的必要参数,没有空隙存在。

Class文件格式采用一种类似C语言结构体的伪结构来存数据,这种伪结构只有两种数据类型:无符号数和表。

无符号数属于基本数据类型,以u1,u2,u4,u8来分别代表1个字节(一个字节是由两位16进制数组成。例如cafe babe:c是一个16进制,a是一个16进制等等。ca组成了一个字节),2个字节,4个字节,8个字节的无符号数,无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型。对应关系可参照下图

将class文件批量转成java文件_将class文件批量转成java文件_06

所有表都习惯性地以”_info”结尾,也就是被两个"_info"包围就可以认为是一张表的数据。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张一张的表组合而成的。

Class文件格式细节分析

Class文件的结构不像XML等描述语言。由于它没有任何分隔符号,所以在其中的数据项,无论是顺序还是数量,都被严格限制。哪个字节代表了什么含义,长度是多少,先后顺序如何,都不允许改变。按顺序包括:

魔数

将class文件批量转成java文件_jvm_07

每个Class文件的头4个字节(U4)称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。使用魔数而不是扩展名(扩展名.java  扩展名.class  扩展名.jar)进行识别主要基于安全方面的考虑。因为扩展名可以随意地改动。(言外之意就是cafe babe是证明这是class文件的唯一标志)。

版本

紧接着的四个字节,第一个U2(第5,6个字节)是次版本号(MinorVersion),第二个U2(第7,8个字节)是主版本号(MajorVersion)。Java的版本是从45开始记起。JDK1.1之后每个JDK大版本发布主版本号向上+1,高版本能向下兼容以前旧版本的Class文件,但不能运行以后版本的Class文件,即使文件格式未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

将class文件批量转成java文件_Java_08

就代表着JDK1.8(16进制的34,换成10进制就是52   JDK1.1---45   JDK1.8---52)。

常量池

常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java习惯不一样的是,这个容量计数器是从1而不是从0开始的。也就是1代表的是无常量,2才是有一个。0可以表示某些指向常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目”的含义。

由图可知,此处的16,实际值为16-1=15个。

将class文件批量转成java文件_java_09

使用Javap -v 反编译结果

常量池共15个数据。

常量池中每一种类型的常量都是一张表。目前截止至JDK13,常量表中分别有17中不同类型的常量。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。

字面量比较接近于Java语言中的常量的概念,比如字符串,声明为final的常量值等。

我们用Jclasslib工具反编译这个Class文件,查看一下常量池中的数据。

将class文件批量转成java文件_java_10

再提符号引用

我们知道,当我们的程序在运行中时,我们堆中的对象要调用方法,是通过对象头中存储的类型指针,通过直接引用,找到具体方法在方法区中的地址。

那么,在类加载过程中,我们需要把Class文件加载到我们运行时数据区,就需要用到我们的符号引用了。了解了符号引用的概念,那么具体符号引用中有哪些细节帮助我们寻找地址呢?

符号引用包含类和接口的全限定名(Fully Qualified Name),字段的名称和描述符(Descriptor),方法的名称和描述符。

访问标志(识别类是什么关键字修饰的)

用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口:是否定义为public类型,是否定义为abstract类型:如果是类的话,是否被声明成final等。识别类的修饰符

类索引,父类索引与接口索引集合

这三项数据来确定这个类的继承关系。

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的类都有父类。因此除了java.lang.Object之外,所有Java类的父类索引都不为0.接口索引集合就用来描述这个这个类实现了哪些接口,这些被实现的接口按implements语句(如果这个类本身是一个接口,则应当是extends语句)后续的接口顺序从左到右排列在这个接口索引集合中。

字段表集合

描述接口或者类中声明的变量,字段(field)包括类级变量(全局级变量或静态变量,需要使用static关键字修饰)以及实例级变量(成员变量,实例化后才会分配内存空间,才能访问)。

字段可以包括的修饰符有字段作用域(public,privbate,protected)。是成员还是类变量(static),是否可变(final),并发可见性(volatile),是否可被序列化(transient)。这些信息在字段表中都是布尔值形式存在的。有这个修饰符就是1,没有就是0。

而字段叫什么名字,字段被定义成什么数据类型,这些都是无法固定的。所以只能引用常量池中的常量来描述。

字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

方法表集合

Class文件存储格式中对方法的描述与字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样。一次包括访问标志,名称索引,描述符索引,属性集合表几项。

描述了方法的定义,但是方法里的Java代码,经过编译器编译成字节码指令之后,存放在属性表集合中的方法属性表集合中一个名为”Code”的属性里面。与字段表集合是类似的。如果父类方法在子类中没有被重写(Override),方法集中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器”<client>”方法和实例构造器<”init”>

属性表集合

存储Class文件,字段表,方发表都又自己的属性表集合,用于描述某些场景专有的信息。比如方法的代码就存储在Code属性表中。

字节码指令

字节码指令

字节码指令属于方法表中的内容。

将class文件批量转成java文件_将class文件批量转成java文件_11

将class文件批量转成java文件_字段_12

Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。

由于限制了 Java 虚拟机操作码的长度为一个字节(即 0~255),这意味着指令集的操作码总数不可能超过 256 条。

大多数的指令都包含了其操作所对应的数据类型信息。例如:

iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据(此处未列举)。

大部分的指令都没有支持整数类型 byte、char 和 short,甚至没有任何指令支持 boolean 类型。大多数对于 boolean、byte、short 和 char 类型数据的操作,实际上都是使用相应的 int 类型作为运算类型

阅读字节码作为了解 Java 虚拟机的基础技能,有需要的话可以去掌握常见指令。

字节码助记码解释地址:https://cloud.tencent.com/developer/article/1333540