Java 实现一次编译到处运行的基础,来源于 Java 虚拟机屏蔽了操作系统的底层细节。使用 class 文件存储编译后的源程序,使得 Java 程序的编译与操作系统解耦。正是因为 Java class 文件的设计与 Java 语言解耦,分别发布了 Java语言规范和 Java 虚拟机规范,使得其他语言如Scala、Groovy、JRuby、JPython 等基于Java 虚拟机的语言按照 class 文件格式要求生成的class 文件也能在虚拟机上运行。

 

class 文件格式

class 文件采用如下的结构存储二进制内容。其中 u2、u4 分别表示占用 2、4 个字节。

{
u4 magic; //魔数,固定为0xCAFEBABE
u2 minor_version; //次版本号
u2 major_version; //主版本号
u2 constant_pool_count; //常量池计数器
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags; //访问标志,声明权限
u2 this_class; //类索引
u2 super_class; //父类索引
u2 interfaces_count; //接口个数
u2 interfaces[interfaces_count]; //接口列表
u2 fields_count; //字段个数
field_info fields[fields_count];//字段列表
u2 methods_count; //方法个数
method_info methods[methods_count]; //方法列表
u2 attributes_count; //属性个数
attribute_info attributes[attributes_count]; //属性列表
}

 

class 文件内容解读

常量池:

存储 class 文件用到的所有的字符串常量、类名、接口名、字段名以及其他常量。class 文件的其他项目往往会引用常量池中的常量,因此常量池容量计数从1开始,0 用于表示其他项目不引用常量池。在常量池中主要存储了字面量和符号引用两大类常量,字面量主要是字符串、final 类型常量值等,符号引用则包括类和接口的全限定名、字段的名称和描述法以及方法的名称和描述符。在前文《 Java 虚拟机类加载机制》中提到的符号引用转换为直接引用中的符号引用就是常量池中的符号引用。

访问标志:

类或接口的访问权限信息,包括 public、final、super、interface、abstract、annotation、enum 几种属性,以及使用 synthetic 表示非 Java 源码生成的代码。

类索引:

this_class 存储常量池中的一个索引,索引处的常量表示 class 文件定义的类或接口。如果这是一个类,super_class 为 0 或存储常量池中的一个索引,索引处的常量表示父类;如果这是一个接口,super_class 存储常量池中的一个索引,索引处的常量一定是 java.lang.Object。通过 this_class 可以确定当前类的全限定名,通过 super_class 可以确定父类的全限定名。

接口列表:

如果这是一个类,存储该类实现的接口列表,按照 implements 后的接口顺序存储;如果这是一个接口,存储该接口的所有父接口列表,按照 extends 后的接口顺序存储。

字段列表:

存储类或接口声明的变量,包括类变量和实例变量。描述了每个变量的信息,包括作用域、static、final、volatile、transient、类型、名称等。其中字段的名称、类型需要引用常量池中的常量来描述。

方法列表:

存储类或接口声明的方法,包括类方法和实例方法。描述了每个方法的信息,包括访问标志、名称索引、描述符索引、属性表集合等。这里仅仅存储了方法的信息,方法的实现代码编译成字节码后存储在属性表集合中的 “ Code ” 属性里面。

属性列表:虚拟机规范定义了大量的属性,class 文件、字段列表、方法列表都可以使用属性描述专有信息。而属性的名称需要引用常量池的常量来表示。方法体中的代码经编译后就存放在名为 Code 的属性中。

 

总结

Java 源程序编译后生成 class 文件而不是二进制可执行文件,通过 Java 虚拟机来解析并执行 class 文件中的程序,实现了“一次编译,到处运行”。在 class 文件中,存储了类或接口的基本信息,如版本号、类名、接口列表、字段列表、方法列表等。