类文件即.class文件,能够在java虚拟机上运行。Java虚拟机不和包括Java在内的任何语言绑定,只与Class文件这种特定的二进制文件格式关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。class文件可以由其他语言编译而来,例如:JRuby、Jython、Scala等。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号和表。无符号属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节,无符号数可以用来描述数据类型、索引引用、数量值或者按照UTF-8编码构成字符串值。表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。
整个Class文件本质上就是一张表,构成这张表的数据项如下:
序号 | 类型 | 名称 | 数量 | 备注 |
---|---|---|---|---|
1 | u4 | magic | 1 | 魔数,确定文件是否是能被虚拟机接受的Class文件 |
2 | u2 | minor_version | 1 | 次版本号 |
3 | u2 | major_version | 1 | 主版本号 |
4 | u2 | constant_pool_count | 1 | 常量池容量计数值 |
5 | cp_info | constant_pool | constant_pool_count-1 | 常量池 |
6 | u2 | access_flags | 1 | 访问标志 |
7 | u2 | this_class | 1 | 类索引 |
8 | u2 | super_class | 1 | 父类索引 |
9 | u2 | interfaces_count | 1 | 接口计数值 |
10 | u2 | interfaces | interfaces_count | 接口索引集合 |
11 | u2 | fields_count | 1 | 字段计数值 |
12 | field_info | fields | fields_count | 字段表集合 |
13 | u2 | methods_count | 1 | 方法计数值 |
14 | method_info | methods | methods_count | 方法表集合 |
15 | u2 | attributes_count | 1 | 属性计数值 |
16 | attribute_info | attributes | attributes_count | 属性表集合 |
注意:Class文件没有任何分隔符,所以上表中的数据项,无论是顺序还是数据量,甚至是数据存储的字节序,都是严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
常量池的入口在主次版本号之后,可以把常量池理解为Class文件中的资源仓库,是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时还是在Class文件中第一个出现的表类型数据项目。常量池中的常量数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值,即:constant_pool_count。该容量计数器是从1而不是0开始的,将第0项常量空出来的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况下就可以把索引值置为0来表示。
常量池中主要存放两大类常量:字面量和符号引用。字面量,如文本字符串、声明为final的常量值等,符号引用包括三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类创建的时候或者运行时解析、翻译到具体的内存地址之中。
常量池中的常量项目结构表如下:
常量 | 项目 | 类型 | 描述 |
---|---|---|---|
CONSTANT_Utf8_info (UTF-8编码的字符串) |
tag | u1 | 值为1 |
length | u2 | UTF-8编码的字符串占用的字节数 | |
bytes | u1 | 长度为length的UTF-8编码的字符串 | |
CONSTANT_Integer_info (整型字面量) |
tag | u1 | 值为3 |
bytes | u4 | 按照高位在前存储的int值 | |
CONSTANT_Float_info (浮点型字面量) |
tag | u1 | 值为4 |
bytes | u4 | 按照高位在前存储的float值 | |
CONSTANT_Long_info (长整型字面量) |
tag | u1 | 值为5 |
bytes | u8 | 按照高位在前存储的long值 | |
CONSTANT_Double_info (双精度浮点型字面量) |
tag | u1 | 值为6 |
bytes | u8 | 按照高位在前存储的double值 | |
CONSTANT_Class_info (类或接口的符号引用) |
tag | u1 | 值为7 |
index | u2 | 指向全限定名常量项的索引 | |
CONSTANT_String_info (字符串类型字面量) |
tag | u1 | 值为8 |
index | u2 | 指向字符串字面量的索引 | |
CONSTANT_Fieldref_info (字段的符号引用) |
tag | u1 | 值为9 |
index | u2 | 指向声明字段的类或者接口描述符 CONSTANT_Class_info的索引项 |
|
index | u2 | 指向字段描述符 CONSTANT_NameAndType的索引项 |
|
CONSTANT_Methodref_info (类中方法的符号引用) |
tag | u1 | 值为10 |
index | u2 | 指向声明字段的类或者接口描述符 CONSTANT_Class_info的索引项 |
|
index | u2 | 指向字段描述符 CONSTANT_NameAndType的索引项 |
|
CONSTANT_InterfaceMethodref_info (接口中方法的符号引用) |
tag | u1 | 值为11 |
index | u2 | 指向声明字段的类或者接口描述符 CONSTANT_Class_info的索引项 |
|
index | u2 | 指向字段描述符 CONSTANT_NameAndType的索引项 |
|
CONSTANT_NameAndType_info (字段或方法的部分符号引用) |
tag | u1 | 值为12 |
index | u2 | 指向该字段或方法名称常量项的索引 | |
index | u2 | 指向该字段或方法描述符常量项的索引 | |
CONSTANT_MethodHandle_info (表示方法句柄) |
tag | u1 | 值为15 |
reference_kind | u1 | 值必须在1~9之间(包括1和9), 它决定了方法句柄的类型。 方法句柄类型的值表示方法句柄的字节码行为 |
|
reference_index | u2 | 值必须是对常量池的有效索引 | |
CONSTANT_MethodType_info (标识方法类型) |
tag | u1 | 值为16 |
descriptor_index | u2 | 值必须是对常量池的有效索引, 常量池在该索引处的项必须是 CONSTANT_Utf8_info结构,表示方法的描述符 |
|
CONSTANT_InvokeDynamic_info (表示一个动态方法调用点) |
tag | u1 | 值为18 |
bootstrap_method_attr_index | u2 | 值必须是对当前Class文件中引导方法表的 bootstrap_methods[]数组的有效索引 |
|
name_and_type_index | u2 | 值必须是对当前常量池的有效索引, 常量池在该索引处的项必须是 CONSTANT_NameAndType_info结构, 表示方法名和方法描述符 |
紧接着常量池后的两个字节是访问标志,访问标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义位public类型;是否定义位abstract类型;如果是类的话,是否被声明位final等。
具体标志如下:
字段表用于描述接口或类中声明的变量。字段包括类级别变量以及实例级别变量,但不包括在方法内部声明的局部变量。字段集合可以包括的信息有:
这些修饰符都是布尔值,要么有某个修饰符,要么没有。字段集合表访问标志如下:
字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外,Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码而言,如果两个字段的描述符不一致,那字段重名就是合法的。
方法表的结构和字段表的结构一样,一次包括访问标志、名称索引、描述符索引、属性表集合。访问标志如下:
属性表集合不在要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以从属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略不认识的属性。
一些属性中的关键常用内容:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类时才能拥有这个属性, 这个属性用于标识这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性, 供新的类型检查验证器检查和处理目标方法的局部变量 和操作数栈所需要的类型是否匹配 |
Signature | 类、方法表、字段表 | JDK1.5中新增的属性, 这个属性用于支持泛型情况下的方法签名, 在Java语言中,任何类、接口、初始化方法或 者成员的泛型签名如果包含了类型变量 或参数化类型,则Signature属性会为它 记录泛型签名信息。由于Java的泛型采 用擦除法实现,在为了避免类型信息被 擦除后导致签名混乱,需要这个 属性记录泛型中的相关信息 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | JDK1.6中新增的属性,SourceDebugExtension属性 用于存储额外的调试信息。譬如在进行 JSP文件调式时,无法通过Java堆栈来定位 到JSP文件的行号,JSR-45为这些非Java编写, 却需要编译成字节码并运行在Java虚拟机 中的程序提供了一个进行调试的标准机制, 使用SourceDebugExtension属性 就可以用于存储这个标准所新加入的调试信息 |
Syntheitc | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | JDK1.5中新增的属性,它使用特征签名代替描述符, 是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类、方法表、字段表 | JDK1.5中新增的属性,为动态注解提供支持。 RuntimeVisibleAnnotations属性用于指明哪些注解是运行时 (实际上就是进行反射调用)可见的 |
RuntimeInvisibleAnnotations | 类、方法表、字段表 | JDK1.5中新增的属性,与RuntimeVisibleAnnotations属性 作用刚好相反,用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotations | 方法表 | JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法参数 |
RuntimeInvisibleParameterAnnotations | 方法表 | JDK1.5中新增的属性,作用与RuntimeInvisibleAnnotations 属性类似,只不过作用对象为方法参数 |
AnnotationsDefault | 方法表 | JDK1.5中新增的属性,用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | JDK1.7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符 |