Class类文件结构
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或者接口并不一定都得定义在文件里,譬如类或接口也可以通过类加载器直接生成。
Class文件是一组以8位字节为基础单位的二进制流,整个数据项目严格紧凑的排列在Class文件之中,中间没有添加任何分隔符,这使得Class文件存储的内容几乎都是程序运行必要的数据,没有空隙存在。当遇到需要占用8位字节以上的空间的数据项时,会按照高位在前的方式分割成若干个8位字节进行存储。
按照Java虚拟机规范的规定,Class文件格式采用C语言结构体的伪结构来存储数据,这种伪结构包含两种数据类型:
- 无符号数
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1、2、4、8个字节的无符号数,无符号数可用来描述数字、索引引用、数量值和按照UTF-8编码构成字符串值。
- 表
- 表是有多个无符号数或者其他表为数据项构成的复合数据类型,所有的表习惯以_info结尾。表用于描述有层次关系的符合结构的数据,整个Class文件实质上就是一张表,由下表构成
魔数与Class文件的版本
每个Class文件的头四个字节为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。这个魔数的值为0xCAFEBABE
。紧接着魔数的4个字节是Class文件的版本号:第5个和第6个字节是次版本号(Minor Version),第7个和第8个字节是主版本号(Major Version)。
CAFE BABE 0000 0034
常量池
紧接着主次版本号之后的就是常量池,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用了Class文件空间最大的数据项目之一,它还是Class文件中第一个出现的表类型数据项目。
在常量池的入口有一项u2类型的数据,代表着常量池容量计数值。
0039 0A00 0D00 1908
常量池中主要存放两大类常量:
- 字面量literal
- 符号引用Symbolic References
字面量比较接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
而符号引用则属于编译原理方面的概念,包括了下面的三类常量:
- 类和接口的权限的名
- 字段的名称和描述符
- 方法的名称和描述符
常量池中每一项常量都是一个表。常量池的项目类型如下表所示:
这14类常量类型均有自己的结构。
tag是标志位,用于区分常量类型;name_index是一个索引值,它指向常量池中一个CONSTANT_UTF8_info类型常量,此常量代表了这个类的全限定名。
下面来看看javap输出的字节码内容。
public class com.jian8.basic.TestCase
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#21 // java/lang/Object."<init>":()V
#2 = Fieldref #22.#23 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #24.#25 // java/io/PrintStream.println:(I)V
#4 = Class #26 // com/jian8/basic/TestCase
#5 = Class #27 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcom/jian8/basic/TestCase;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 i
#18 = Utf8 I
#19 = Utf8 SourceFile
#20 = Utf8 TestCase.java
#21 = NameAndType #6:#7 // "<init>":()V
#22 = Class #28 // java/lang/System
#23 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(I)V
#26 = Utf8 com/jian8/basic/TestCase
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (I)V
上面的代码清单中,我们会发现一些似乎没有在代码中用过的常量,如“I”,“V”,“<init>”,“LineNumberTable”,“LocalVariableTable”
等。这部分自动生成的常量与后面将要讲到额字段表、方发表、属性表相关。
访问标志
在常量池结束之后,紧接着的两个字节代表访问标志(flags),这个标志用于识别一些类或者接口层次的访问信息,如下表示:
类索引、父索引、接口索引集合
类索引、父索引都是一个u2类型的数据。而接口索引集合是一组u2类型的数据的集合,Class文件中由三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名
,父类索引用于确定这个类的父类全限定名
。由于Java语言不支持多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类。接口索引集合就用来描述这个类实现了哪些接口
,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口索引集合中。
字段表集合
字段表field_info用来描述接口或类中声明的变量。字段包括类级变量
以及实例级变量
,但不包括在方法内声明的局部变量。
这里说明一下“简单名称”,“描述符”,“全限定名”这三种特殊字符串的概念
全限定名和简单名称很好理解。像com/jian8/basic/TestCase就是TestCase这个类的全限定名,仅仅是把类全名中的.
换成了/``。简单名称是指没有类型和参数修饰的方法或者字段名称。
描述符是用来描述字段的数据类型、方法数据类型、方法的参数列表(包括数量、类型及顺序)的返回值。
LineNumberTable属性
用来描述Java源码行号和字节码行号(字节码的偏移量)之间的对应关系。不是运行时不需的属性,但默认会加到Class文件中。可以在javac从中使用-g:none or -g:lines
选项取消或要求生成这些信息。
LocalVariableTable属性
用来描述栈帧中局部变量的变量与Java源码定义的变量之间的关系
,它也不是运行时不需的属性,但默认会生成到Class文件中,可以在javac从中使用-g:none or -g:vars
选项取消或要求生成这些信息。如果没有这个属性,最大的影响就是其他人引用这个方法时,所有的参数名称都会丢失,IDE将会使用诸如arg0,arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是调试期间无法根据参数名称从上下文获得参数值。
ConstantValue属性
通知虚拟机自动为静态变量赋值,只有static关键字修饰的变量才可以使用这项属性。对于非static类型的变量的复制是在实例构造器方法中进行的,而对于类变量,则有两种方式可以选择:
- 在类构造器方法中
- 使用ConstantValue属性
目前Sun javac编译器的选择是:如果同时使用final和static
修饰一个变量,并且这个变量的数据类型是基本数据类型或者String的话,就生成ContantValue属性来进行初始化,如果这个变量没有final修饰,或者并非基本类型及字符串,则将会选择在<clinit>
方法中进行初始化
InnerClasses属性
用来记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。