计算机只认识0和1,所以我们编写的程序要经过编译器编译成计算机所能识别的指令,随着虚拟机的发展编译成二进制本地机器码已经不是唯一的选择,越来越多的程序语言选择了与操作系统和机器指令无关的格式作为编译后的存储格式.
本篇文章讲解了 Class 文件结构中的各个组成部分,以及每个部分的定义,数据结构和使用方法.

1.1 无关的基石

Sun 公司发布了各种平台上不同的虚拟机版本,这些不同平台上的虚拟机只识别字节码文件,这种字节码文件就是构成平台无关性的基石.除了平台无关性,语言无关性也有很大的优势, Java 虚拟机不和包括 Java 在内的所有语言绑定,只与”Class 文件”这种特定的二进制文件格式所关联, Class 文件包含了 Java 虚拟机指令和符号表以及若干其他辅助信息.虚拟机不关心 Class 的来源是何种语言.

1.2 Class 文件结构

Class 文件采用类似 C 语言结构体的伪结构体来存储数据,这种伪结构只有两种数据类型,无符号数和表.
无符号数可以描述数字,索引引用,数量值或者按照 UTF-8编码构成的字符串值.
表是由无符号数或者其他表作为数据项构成的复合数据类型

1.2.1 魔数与 Class 文件版本

每个 Class 文件的头4个字节是魔数,它的唯一作用是确定当前文件能否被虚拟机进行加载.很多文件存储格式否使用魔数来进行身份识别,使用魔数而不使用扩展名是因为扩展名随意更改.
紧接着魔数后的4个字节是版本号,高版本的虚拟机能够向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件.

1.2.2 常量池

常量池中有14种常量类型,每种类型都有自己的结构.
由于这些常量的个数是不固定的,所以在常量池的入口放置一项 u2类型的数据,代表常量池容量计数值.
虚拟机的常量池主要存放两大类常量:字面量和符号引用.
字面量接近于 Java 语言层面的常量概念,如字符串,声明为 final 的常量值等.
符号引用则数据编译原理方面的概念,包括:类和结构的全限定名,字段和名称的描述符,方法的名称和描述符.

1.2.3 访问标识

在常量池结束后的两个字节代表访问标识,用于识别类或者接口的访问信息,包括:这个 Class 是类还是接口,是否定义为 public 类型,是否定义为 abstract 类型等.

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

Class 文件由这三项来确定类的继承关系,类索引确定这个类的全限定名,父类索引确定这个类父类的全限定名,接口索引集合用来描述这个类实现了那些接口.

1.2.5 字段表集合

字段表用来描述类或接口中声明的变量,包括类级变量以及实例级变量,但不包括方法内部声明的局部变量.

1.2.6 方法表集合

方法表的结构如字段一样,包含了访问标识,名称索引,描述符索引几项.

1.2.7 属性表集合

  • Code 属性

当代码经过编译之后,最终字节码指令存储在 Code 属性内.

  • Exceptions 属性

列举出方法可能抛出的受查异常

  • LineNumberTable 属性

描述 Java 源代码行号与字节码行号之间的关系.

  • LocalVarableTable 属性

描述栈帧中局部变量表中的变量与 Java 源代码中定义的变量之间的关系.

  • SourceFile 属性

记录生成这个 Class 文件的源码文件名称.

  • ConstantValue 属性

通知虚拟机自动为静态变量赋值.

  • InnerClasses 属性

记录内部类与宿主类之间的关联.

  • Deprecated 及 Synthetic 属性

这两个属性都数据标志类型的布尔属性.
Deprecated 用于表示某个类,字段或者方法被程序作者定位不再推荐使用.
Synthetic 代表此字段或者方法并不是由 Java 源码产生的,而是由编辑器自行添加.

  • StackMapTable 属性

一个复杂的变长属性,位于 Code 属性的属性表中.这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的是代替以前比较消耗新能的基于数据流分析的类型推导验证器.

  • Signature 属性

可以出现于类,属性表和方法表结构的属性表中,在 JDK1.5之后,任何类,接口,方法的泛型签名如果包含了类型变量或参数化类型,则 Signature 属性会记录泛型签名信息.
因为 Java 语言的泛型采用擦除方法实现,在字节码中,泛型信息编译之后统统被擦除掉.
使用擦除的原因是实现简单,运行期也能节省一些类型所占的内存空间,坏处是运行期无法像 C# 等有真泛型支持的语言那样,将泛型类型与用户定义的类型信息同等对待, Signature 就是为了弥补这个缺陷,如 Java 的反射 API 能够获取泛型类型,就是来源这个属性.

1.3 字节码指令简介

由一个字节长度的,代表某种特定操作含义的数据以及后面的零到多个代表此操作所所需参数构成.
由于Java 虚拟机面向操作数栈而不是寄存器及架构,所以大多数的指令都不包含操作数.

1.3.1 字节码与数据类型

在 Java 虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息.如: iload 指令用于从局部变量表中加载 int 类型的数据到操作数栈中.

1.3.2 加载与存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输.
如:将一个局部变量加载到操作栈: iload
将一个数值从操作数栈存储到局部变量表: istore

1.3.3 运算指令

将两个操作数栈上的数据进行某种特定操作,并将结构重新存入操作栈顶.

1.3.4 类型转化指令

对两个数值类型进行相互转换,一般用于实现用户代码中的显式类型转换操作.

1.3.5 操作数栈管理指令

操作一个普通操作数据结构中的堆栈一样, Java 虚拟机提供了一些用于直接操作操作数栈的指令,包括:

  • 将操作数栈的栈顶一个或者两个元素出栈: pop,pop2
  • 将栈最顶端的两个数值互换: swap.

1.3.6 控制转移指令

可以让 Java 虚拟机有条件或无条件地从指定位置指令而不是控制转移指令的下一条指令继续执行程序.

1.4 公有设计和私有设计

Java 虚拟机的实现运行在满足虚拟机规范的约束下对具体实现做出修改和优化.只要优化后的 Class 文件依然可以被正确读取,实现者可以选择任何方式去实现这些语义.

1.5 Class 文件结构的发展

自 Class 文件发布十多年以来, Class 文件的主体结构,字节码指令的语义和数量几乎没有改动,所有的改动都集中在 访问标识,属性表这些在设计上可扩展的数据结构中添加内容.
Class 文件格式锁具备的平台中立,紧凑,稳定和可扩展的特点,是 Java 技术体系实现平台无关,语言无关两项特性的重要支柱.