【JVM】类文件结构

学习JVM居然能忽略类文件结构?不存在的!类文件结构作为理解JVM入门级的垫脚石,你不能不清楚它的存在及意义 °(°ˊДˋ°) °


引言

如今的计算机仍然只能识别0和1,但将我们编写的程序编译成二进制本地机器码已不再是唯一的选择,越来越多的程序语言选择了与操作系统和机器指令集无关的、 平台中立的格式作为程序编译后的存储格式。各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石,而且语言无关性正越来越被开发者所重视。Java虚拟机不和包括Java在内的任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。


类文件结构的相关概念与属性

类文件定义

Class文件是一组以8位字节为基础单位的二进制流,各项数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,如果是超过8位字节以上空间的数据项,则会按照高位在前的方式(Big-Endian)分割成若干个8位字节进行存储。

来看一个类文件二进制流实例:

2jo44.png
其中一个字节为2位16进制数,图中的‘cafe’就是为两个字节

魔数与文件版本

魔数是每个class文件的4个字节组成的数,如图中的‘cafe babe’就是魔数,魔数的唯一作用就是用来判断此class文件是否是能被虚拟机接受的文件。很多种文件都是用到了魔数进行文件类别的识别,如:.gif, .jpeg等

文件版本号就是紧跟在魔数后的四个字节,其中,第5 6个字节表示的是次版本号,第7 8个字节表示的是主版本号。如图中的0000为次版本号,0034为主版本号,Java的版本号是从45开始的,JDK1.1之后每个JDK大版本发布,其主版本号就向上加1,这就让高版本的JDK能向下兼容低版本的class文件,但是不能向上兼容高版本的class文件,因为虚拟机是拒绝执行超过它版本号的class文件

常量池

常量池的入口紧跟在主次版本号之后,向后的两个字节为常量池的个数,再向后就是从常量1….到常量n,常量池是class文件的数据仓库,也是占用class文件空间最大的数据项目之一,它是与class文件的其他项目关联最多的数据类型。
常量池中主要存放着两大元素

  1. 字面量:类似于常量概念,如final修饰的常量值等等
  2. 符号引用:包含类和进口的全限定名、字段的名称和描述符、方法的名称和描述符这三种常量,属于编译方面的概念。

当JVM运行时,需要从常量池中获取对应的符号引用,然后在类创建或运行时进行解析,得到真正的内存地址。

访问标志

访问标志是在常量池继续向后的两个字节,这个标志(access-flags)表示一些类或接口的访问信息,比如,这个Class是一个类呢还是一个接口呢,它是被什么限定符所修饰的?public?abstract?final?这些都在访问标志中被标明。

类索引-父索引-接口索引

类索引(this_class)与父索引(super_class)都是一个两个字节的数据集合,因为Java的单一继承所以让类索引和父索引只能有一个,以此类推,接口索引就不再只是一个两个字节的数据集合了,而是一组两个字节的数据集合,前两个字节代表着接口索引的个数,后面的字节分别就代表了接口1….接口n,其中类索引是用来确定这个类的全限定名,父所以与接口索引用来确定这个类的继承连接关系,接口索引的顺序是按照implements(或extends)后的顺序以此展开的。

字段表集合

字段表中存放用于描述类或接口中声明的变量信息,也是由一个两个字节的数据表示字段个数后面字节代表字段1….字段n,其中包括类级和实例级变量(static修饰符来区分)但是其中不包括局部变量也不包括从超类或父类中继承来的字段,还有一些是有着特殊含义的字段,如内部类通常会添加指向外部类的字段用来保持对外部类的访问性。其中字段的通常信息包括:

  1. 字段的作用域(public protected private default修饰符区分);
  2. 变量的级别(类级变量或实例级变量);
  3. 可变性(final修饰符区分);
  4. 并发可见性(volatile修饰符);
  5. 可否序列化(reansient修饰符);
  6. 字段的数据类型(int char double float等修饰符);
  7. 字段的名称等等。

方法表集合

与字段表一致的是方法集合在class文件中采用了相同的存储方式,也是由一个两个字节的数据表示方法个数后面字节代表方法1….方法n,其中方法表的集合的信息通常包含:

  1. 访问标志(access_flags);
  2. 名称索引(name_index);
  3. 描述符索引(descriptor_index);
  4. 属性表集合(attributes)这个里面包含的是类文件方法里的Java代码经过虚拟机编译后的字节码指令,这些指令都被放在了一个叫属性表的集合里面的“code”属性里。

解释一下字节码指令:字节码指令是指JVM的指令由一个字节长度的、代表着某种特定操作含义的数字(操作码,Opcode)以及跟随其后的0至多个爱表此操作所需参数(操作数,Operands)构成。字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构,由于限制了Java虚拟机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不可能超过256 条;又由于Class文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时从字节中重建出具体数据的结构。

属性表集合

在class 文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息,它也是由一个两个字节的数据表示属性个数后面字节代表属性1….方属性n。其中的属性包括:

  1. code属性:这个在方法表集合中提到过,类文件方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。但是要注意,抽象类和接口的方法是没有code属性的;
  2. Exception属性:这个应该是很常见的,这个是一个异常类的统称,Exception属性就是用来列举出方法中可能抛出的(thorows关键字后的)受检查异常;
  3. LineNumberTable属性:用于描述 Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到 Class 文件之中,可以在 javac 中分别使用 -g:none 或 -g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable 属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。
  4. LocalVariableTable属性:用于描述栈帧中局部变量表中的变量与 Java 源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到 Class 文件之中,可以在 Javac 中分别使用 -g:none 或 -g:vars 选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE 将会使用诸如 arg0、arg1 之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不变,而且在调试期间无法根据参数名称从上下文获得参数值。
  5. SourceFile属性:用于记录生成这个 Class 文件的源码文件名称。这个属性也是可选的,可以分别使用 javac 的 -g:none 或 -g:source 选项来关闭或要求生成这项信息。在 Java 中,对于大多数的类来说,类名和文件名是一致的,但是又一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。
  6. ConstantValue属性:作用是通知虚拟机自动为静态变量赋值。只有被 static 关键字修饰的变量(类变量)才可以使用这项属性。类似 “int x = 1” 和 “static int x = 1” 这样的变量定义在 Java 程序中是非常常见的事情,但虚拟机对这两种变量赋值的方式和时刻都有所不同。对于非 static 类型的变量(也就是实例变量)的赋值是在实例构造器 方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器 方法中或者使用 ConstantValue 属性。目前 Sun Javac 编译器的选择是:如果同时使用 final 和 static 来修饰一个变量(按照习惯,这里称 “常量” 更贴切),并且这个变量的数据类型是基本类型或者 java.lang.String 的话,就生成 ConstantValue 属性来进行初始化,如果这个变量没有被 final 修饰,或者并非基本类型及字符串,则将会选择在 方法中进行初始化。
  7. InnerClass属性:用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成 InnerClass 属性。
  8. Deprecated和Synthetic属性:Deprecated 和 Synthetic 两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。Deprecated 属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用 @deprecated 注释进行设置。
    Synthetic 属性代表此字段或者方法并不是由 Java 源码直接产生的,而是由编译器自行添加的,在 JDK 1.5 之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们访问标志中的 ACC_SYNTHETIC 标志位,其中最典型的例子就是 Bridge Method。所有由非用户代码产生的类、方法及字段都应当至少设置 Synthetic 属性和 ACC_SYNTHETIC 标志位中的一项,唯一的例外是实例构造器 “” 方法和类构造器 “” 方法。
  9. StackMapTable属性:在 JDK 1.6 发布后增加到了 Class 文件规范中,它是一个复杂的变长属性,位于 Code 属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
  10. Signature属性:在 JDK 1.5 发布后增加到了 Class 文件规范之中,它是一个可选的定长属性,可以出现于类、属性表和方法表结构的属性表中。在 JDK 1.5 中大幅增强了 Java 语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则 Signature 属性会为它记录泛型签名信息。之所以要专门使用这样一个属性去记录泛型类型,是因为 Java 语言的泛型采用的是擦除法实现的伪泛型,在字节码(Code 属性)中,泛型信息编译(类型变量、参数化类型)之后都通通被擦除掉。使用擦除法的好处是实现简单(主要修改 Javac 编译器,虚拟机内部只做了很少的改动)、非常容易实现 backport,运行期也能够节省一些类型所占的内存空间。但坏处是运行期就无法像 C# 等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得泛型信息。Signature 属性就是为了弥补这个缺陷而增设的,现在 Java 的反射 API 能够获取泛型类型,最终的数据来源也就是这个属性。
  11. BootstrapMethods属性:在 JDK 1.7 发布后增加到了 Class 文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存 invokedynamic 指令引用的引导方法限定符。

结语

Java类文件结构是java技术体系的重要基础之一,类文件是JVM对于编译首先要识别的入口,这是个数据和指令的开端,所以想要深入理解Java虚拟机,就不能不对class文件结构进行一个详细的认知。

如果觉得还不错的话,把它分享给朋友们吧(ง •̀_•́)ง