0%

第6章: 类文件结构

概述

计算机只认识0和1,但是最近10年虚拟机以及大量建立在虚拟上的语言的发展,将我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。

无关性的基石

Sun公司及其他虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现程序的“一次编写,到处运行”。而各种不同平台的虚拟机以及平台无关的字节码(ByteCode)是构成平台无关性的基石。

到目前为止,或许大部分程序员都还认为Java虚拟机执行Java程序是一件理所当然的事情,但在Java发展之初,Java的规范就分为Java语言规范以及Java虚拟机规范,时至今日,Java语言之外已经有一大批语言运行在Java虚拟机之上,如 Groovy、JRuby、Jython等。

Java 虚拟机不和包括Java在内的任何语言绑定,它只与“Class 文件”这种特定的二进制文件格式所关联,任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。

Class 类文件的结构

Class 文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有任何分隔符。遇到需要占用8位字节以上空间的数据项时,会按照高位在前的方式分割成若干个8位字节进行存储。

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

无符号数属于基本的数据类型,以u1、u2、u4、u8分别代表1、2、4、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8 编码构成的字符串值。

表示由多个无符号数或者其他表作为数据项的复合数据类型,表用于描述有层次关系的复合结构的数据,整个Class文件本质就是一张表。

魔数与Class文件的版本

Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。Class 文件的魔数值为 0xCAFEBABE (咖啡宝贝?),很有浪漫气息。紧接着魔数的4个字节存储着Class文件的版本号,高版本的JDK能乡下兼容以前版本的Class文件,但不能运行以后版本的Class文件。

很多文件存储标准中都使用魔数来进行身份识别,如gif和jpeg等在文件头中都存有魔数。使用魔数而不是扩展名来识别主要是基于安全考虑: 文件扩展可以随意改动。

魔数示意图

常量池

版本号之后是常量池入口,该区域可以理解为Class文件中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。

常量池中主要存放两大类常量:字面量(Literal) 和 符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如 文本字符串、声明为final的常量值等,而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

Java代码在javac编译的时候,并不像c和c++那样有“连接”这一步骤,而是在虚拟机加载 Class 文件的时候进行动态连接,也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息。虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时解析、翻译到具体的内存地址中。

访问标志

常量池结束后,紧接着2个字节代表访问标志(access_flags
),这个标志用于识别一些类或者接口层次的访问信息,包括: 这个Class 是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等等。

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

类索引(this_class) 和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,class 文件中由这三项数据来确定这个类的继承关系。

其中,类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于Java不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此,父类索引都不为0。接口索引集合用来描述这个类实现了哪些接口,按照implement语句后的接口顺序从左到右排列在接口索引集合中。

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量,包括类级变量和实例级变量,不包括方法内的局部变量。这个描述可以包括的信息有: 字段的作用域(public、private、protected修饰符)、实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile 修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符) 等等。

方法表集合

Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,只是些微的区别,比如volatile关键字和transient关键字不能修饰方法 。

属性表集合

在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息(这一块内容也没耐心看下去,下次看明白再补)。

字节码指令简介

这章离App开发比较远,因此第一遍的时候先略过。

公有设计和私有实现

理解公有设计与私有实现之间的分界线是非常有必要的,Java虚拟机实现必须能够读取Class文件并精确实现包含在其中的Java虚拟机代码的语义。按照Java虚拟机规范一成不变地逐字实现其中要求的内容是一种可行的途径,但是一个优秀的虚拟机实现,在满足虚拟机规范的约束下对具体实现做出修改和优化也是完全可行的。虚拟机实现的方式主要有以下两种:

  • 将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集。
  • 将输入的Java虚拟机代码在加载或执行翻译成宿主机CPU的本地指令集(即 JIT 代码生成技术)

Class 文件结构的发展

略。

谢谢你的鼓励