1. 平台无关性(跨平台\可移植性)
平台无关性是 Java 的核心优势,Java 在刚刚诞生之时曾经提出一个非常著名的宣传口号:“一次编写,到处运行(Write Once, Run Anywhere)”。平台无关性的基础是虚拟机和字节码存储格式,Java 虚拟机只与“Class 文件”这种特定的二进制文件格式所关联,Class 文件中包含了 Java 虚拟机指令集和符号表以及若干其他辅助信息。
2. Class 类文件的结构
Class 文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件之中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
根据 Java 虚拟机规范的规定,Class 文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
-
无符号数
-
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
-
表
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表,它由下表所示的数据项构成。
类型 名称 数量 u4 magic 1 u2 minor_version 1 u2 major-version 1 u2 constant_pool_count 1 cp_info constant_pool constant_pool_count - 1 u2 access_flags 1 u2 this_class 1 u2 super_class 1 u2 interfaces_count 1 u2 interfaces interfaces_count - 1 u2 fields_count 1 field_info fields fields_count - 1 u2 methods_count 1 method_info methods methods_count - 1 u2 attributes_count 1 attribute_info attributes attributes_count - 1
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。
2.1 魔数与 Class 文件的版本
每个 Class 文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如 gif 或者 jpeg 等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以自由的选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。Class 文件的魔数值为:0xCAFEBABE。
紧接着魔数的4个字节存储的是 Class 文件的版本号:第5个和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java 的版本号是从45开始的,JDK 1.1之后的每个 JDK 大版本发布主版本号向上加1(JDK 1.0~1.1 使用了45.0~45.3的版本号),高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并没有发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。
2.2 常量池
紧接着主版本号之后的是常量池入口,常量池可以理解为 Class 文件之中的资源仓库,它是 Class 文件结构中与其他项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项目之一,同时它还是在 Class 文件中第一个出现的表类型数据项目。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的数据,代表常量池容量计数值(constant_pool_count)。与 Java 中语言习惯不一样的是,这个容量计数是从1而不是0开始的。在 Class 文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置位0来表示。Class 文件结构中只要常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
Java 代码在进行 Javac 编译的时候,并不像 C 和 C++ 那样有“连接”这一步骤,而是在虚拟机加载 Class 文件的时候进行动态连接。也就是说,在 Class 文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
2.3 访问标志
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。具体的标志位以及标志的含义见下表。
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为 public 类型 |
ACC_FINAL | 0x0010 | 是否被声明为 final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码指令的新语义,incokespecial 指令的语意在 JDK 1.0.2发生过改变,为了区别这条指令使用哪种语意,JDK 1.0.2之后编译出来的类这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,此标志值为真,其它类值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
2.4 类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于 Java 语言不允许多重继承,所以父类索引只有一个,除了 java.lang.Object 外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有的 Java 类的父类索引都不为0。接口索引集合就是用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序从左到右排列在接口索引集合中。
2.5 字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段表的格式如下表。
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
2.6 方法表集合
方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项、。这些数据项目的含义也非常类似,仅在访问标志和属性表集合的可选项中有所区别。
2.7 属性表集合
属性表(attribute_info)与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。为了能正确解析Class文件,《Java虚拟机规范(第2版)》中预定义了9项虚拟机实现应当能识别的属性,而在最新的《Java虚拟机规范(Java SE 7)》版中,预定义属性已经增加到21项,具体内容见下表。
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java 代码编译成的字节码指令 |
ConstantValue | 字段表 | final 关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为 deprecated 的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code 属性 | 方法的局部变量描述 |
StackMapTable | Code 属性 | JDK 1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Signature | 类、方法表、字段表 | JDK 1.5中新增的属性,这个属性用于支持泛型情况下的方法签名,在 Java 语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(TypeVariables)或参数化类型(ParameterizedTypes),则 Signature 属性会为它记录泛型签名信息。由于 Java 的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | JDK 1.6中新增的属性,SourceDebugExtension 属性用于存储额外的调试信息。譬如在进行 JSP 文件调试时,无法通过 Java 堆栈来定位到JSP文件的行号,JSR-45规范为这些非 Java 语言编写,却需要编译成字节码并运行在 Java 虚拟机中的程序提供了一个进行调试的标准机制,使用 SourceDebugExtension 属性就可以用于存储这个标准所新加入的调试信息 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成 |
LocalVariableTypeTable | 类 | JDK 1.5中新增的属性,它使用特征签名代替描述符,是为了引人泛型语法之后能描述泛型参数化类型 而添加 |
RuntimeVisibleAnnotations | 类、方法表、字段表 | JDK 1.5中新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations 属性用于指明哪些注 解是运行时(实际上运行时就是进行反射调用)可见的 |
RuntimeInvisibleAnnotations | 类、方法表、字段表 | JDK 1.5中新增的属性,与 RuntimeVisibleAnnotations 属性作用刚好相反,用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParamterAnnotations | 方法表 | JDK 1.5中新增的属性,作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法参数 |
RuntimeInvisibleParamterAnnotations | 方法表 | JDK 1.5中新增的属性,作用与 RuntimeInvisibleAnnotations 属性类似,只不过作用对象为方法参数 |
AnnotationDefault | 方法表 | JDK 1.5中新增的属性,用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | JDK 1.7中新增的属性,用于保存 invokedynamic 指令引用的引导方法限定符 |