总览
.class文件结构
一. 语言无关性
Java 虚拟机的设计者在设计之初就考虑并实现了其它语言在 Java 虚拟机上运行的可能性.所以并不是只有 Java 语言能够跑在 Java 虚拟机上,时至今日诸如 Kotlin、Groovy、Jython、JRuby 等一大批 JVM 语言都能够在 Java 虚拟机上运行.它们和 Java 语言一样都会被编译器编译成字节码文件,然后由虚拟机来执行.所以说类文件(字节码文件)具有语言无关性.
二. Class 文件结构
1.Class 文件是一组以 8 位字节 为基础单位的二进制流,各个数据严格按照顺序紧凑的排列在 Class 文件中,中间无任何分隔符
2.当遇到需要占用 8 位字节以上空间的数据项时,会按照高位在前的方式分割成若干个 8 位字节进行存储.
3.Java 虚拟机规范规定 Class 文件格式采用一种类似与 C 语言结构体的微结构体来存储数据,这种伪结构体中只有两种数据类型:无符号数和表.
- 无符号数属于基本的数据类型,以 u1、u2、u4、u8来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码结构构成的字符串值.
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以「_info」结尾.表用于描述有层次关系的复合结构的数据,整个 Class 文件就是一张表,它由下表中所示的数据项构成. | 类型 | 名称 | 数量 | | —- | —- | —- | | u4 | magic 魔术 [0xCAFEBABE] | 1 | | u2 | minor_version 主版本号 [0x0034] JDK1.8 [0x0035] JDK1.9 | 1 | | u2 | major_version 次版本号 [0x0034] JDK1.8 [0x0035] JDK1.9 | 1 | | u2 | constant_pool_count 常量池大小 (从1开始) | 1 | | cp_info | constant_pool 常量池表,每一项u1标记位开始(通过标记位确定类型) | constant_pool_count - 1 | | u2 | access_flags 访问标志 16位[0/1]标记 | 1 | | u2 | this_class 类索引、与集合 | 1 | | u2 | super_class 父类索引 | 1 | | u2 | interfaces_count 接口索引大小(从0开始) | 1 | | u2 | interfaces 接口索引组 | interfaces_count | | u2 | fields_count 字段大小(从0开始) | 1 | | field_info | fields 字段表 | fields_count | | u2 | methods_count 方法大小 | 1 | | method_info | methods 方法表 | methods_count | | u2 | attributes_count | 1 | | attribute_info | attributes | attributes_count |
2.1 魔数与 Class 文件版本
每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 .class文件.之所以使用魔数而不是文件后缀名来进行识别主要是基于安全性的考虑,因为文件后缀名是可以随意更改的.Class 文件的魔数值为「0xCAFEBABE」.
紧接着魔数的 4 个字节存储的是 Class 文件的版本号:第 5 和第 6 两个字节是次版本号(Minor Version),第 7 和第 8 个字节是主版本号(Major Version).高版本的 JDK 能够向下兼容低版本的 Class 文件,虚拟机会拒绝执行超过其版本号的 Class 文件.
2.2 常量池
主版本号之后是常量池入口,常量池可以理解为 Class 文件之中的资源仓库,它是 Class 文件结构中与其他项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项目之一,同是它还是 Class 文件中第一个出现的表类型数据项目.
因为常量池中常量的数量是不固定的,所以在常量池入口需要放置一个 u2 类型的数据来表示常量池的容量「constant_pool_count」,和计算机科学中计数的方法不一样,这个容量是从 1 开始而不是从 0 开始计数.之所以将第 0 项常量空出来是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达「不引用任何一个常量池项目」的含义,这种情况可以把索引值置为 0 来表示.
Class 文件结构中只有常量池的容量计数是从 1 开始的
第0项常量具有特殊意义,如果某些指向常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可以将索引值置为0来表示
其它集合类型,包括接口索引集合、字段表集合、方法表集合等容量计数都是从 0 开始
常量池中主要存放两大类常量:字面量和符号引用.
- 字面量比较接近 Java 语言层面的常量概念,如字符串、声明为 final 的常量值等.
- 符号引用属于编译原理方面的概念,包括了以下三类常量:
- 类和接口的全限定名
- 字段的名称和字段的数据类型
- 方法的名称和字段的数据类型、参数列表、返回值
Java代码在进行Java编译的时候,并不像C和C++那样有”连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接.也就是说,在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法被虚拟机使用的.当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址之中
常量池中的每一项都是一个表,总计14个类型,
这14个表的开始第一个字节是一个u1类型的Tag,用来标识是哪一种常量类型
类型 | 标志 | 含义 | 内容 |
---|---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 | Tag U1 1length U2 UTF-8编码的字符串的长度bytes U1 长度为length的UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 | Tag U1 3bytes U4 按照高位在前的int值 |
CONSTANT_Float_info | 4 | 浮点型字面量 | Tag U1 4bytes U4 按照高位在前的float值 |
CONSTANT_Long_info | 5 | 长整形字面量 | Tag U1 5bytes U8 按照高位在前的long值 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 | Tag U1 6bytes U8 按照高位在前的double值 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 | Tag U1 7index U2 指向CONSTANT_String_info的索引 |
CONSTANT_String_info | 8 | 字符串类型字面量 | Tag U1 8index U2 指向字符串字面量的索引 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 | Tag U1 9index U2 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项index U2 指向字段描述符CONSTANT_NameAndType_info的索引项 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 | Tag U1 10index U2 指向声明方法的类描述符CONSTANT_Class_info的索引项index U2 指向名称及类描述符CONSTANT_NameAndType_info的索引项 |
CONSTANT_InterfaceMethod_info | 11 | 接口中方法的符号引用 | Tag U1 11index U2 指向声明方法的接口描述符COSNTANT_Class_info的索引项index U2 指向名称及类描述符CONSTANT_NameAndType_info的索引项 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 | Tag U1 12index U2 指向该字段或方法名称常量池的索引index U2 指向该字段或方法描述符常量池的索引 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | Tag U1 15referencekind U2 值必须在1-9之间,决定了方法句柄的类型,方法句柄累心的值表示方法句柄的字节码行为reference index U2 值必须是对常量池的有效索引 |
CONSTANT_MethodType_info | 16 | 标识方法类型 | Tag U1 16descriptor_index U2 值必须是对常量池的有效索引,常量池在改索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | Tag U1 18bootstrap_method_attrindex U2 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引name_and_type_index U2 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是COSTANT_NameAndType_info结构,表示方法名和方法描述符 |
2.3 访问标志
两个字节代表访问标志,用来标识类或接口的访问信息
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为 public 类型 |
ACC_FINAL | 0x0010 | 是否被声明为 final |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码指令 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,此标志值为真,其它类值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
access_flags 中一共有 16 个标志位可以使用,当前只定义了其中的 8 个,没有使用到的标志位要求一律为 0.
2.4 类索引、父类索引与接口索引集合
类索引 / 父类索引 / 接口索引用于确定全限定名
1.类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据
2.接口索引集合(interfaces)是一组 u2 类型的数据集合
Class 文件中由这三项数据来确定这个类的继承关系
- 类索引用于确定这个类的全限定名
- 父类索引用于确定这个类的父类的全限定名
- 接口索引集合用于描述这个类实现了哪些接口
类索引和父类索引都是一个索引,这个索引指向常量池中的CONSTANT_Class_info类型的常量.然后再CONSTANT_Class_info常量中的索引就可以找到常量池中类型为CONSTANT_Utf8_info的常量,而这个常量保存着类的全限定名
接口索引集合首先是一个u2的接口索引大小,后面结构于类/父类索引一致
2.5 字段表集合
字段(field)包括类变量(类静态变量)和实例变量,但不包括方法内部声明的局部变量
字段表集合中不会列出从父类或父接口中继承的字段,但是可能列出原本Java代码之中不存在的字段,如:内部类为了保持对外部类的访问性,自动添加指向外部类实例的字段
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | access_flags | 1 | 修饰符标记位 |
u2 | name_index | 1 | 代表字段的名称,占2字节,是一个对常量池的引用 |
u2 | descriptor_index | 1 | 代表字段的类型,占2个字节,是一个对常量池的引用 |
u2 | attributes_count | 1 | 属性计数器 |
attribute_info | attributes | attributes_count | 属性表集合 |
字段修饰符放在 access_flags 中,它与类中的 access_flag 非常相似,都是一个 u2 的数据类型.
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否为 public |
ACC_PRIVATE | 0x0002 | 字段是否为 private |
ACC_PROTECTED | 0x0004 | 字段是否为 protected |
ACC_STATIC | 0x0008 | 字段是否为 static |
ACC_FINAL | 0x0010 | 字段是否为 final |
ACC_VOLATILE | 0x0040 | 字段是否为 volatile |
ACC_TRANSIENT | 0x0080 | 字段是否为 transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动生成 |
ACC_ENUM | 0x4000 | 字段是否为 enum |
descriptor_index可以描述字段的数据类型.不过,对于方法的描述符来说就要复杂一些,因为一个方法除了返回值类型,还有参数类型,而且参数的个数还不确定.根据描述符规则,这些类型都使用一个大写字母来表示,如下表:
标识字符 | 含义 |
---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
V | void |
L | 对象类型 如Ljava/lang/Object |
对于数组类型,每一个维度将使用一个前置的“[”字符来描述.比如定义一个java.lang.String类型的二维数组,将记录为”[[Ljava/lang/String”,一个double数组”double[]”将标记为”[D”.
当描述符用来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号”()”内.比如方法void inc()的描述符是:()V.方法java.lang.String toString()的描述符是:()Ljava/lang/String.方法int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)的描述符是:([CII[CIII)I.
2.6 方法表集合
.class 文件中对方法的描述和对字段的描述是完全一致的,方法表中的结构和字段表的结构一样.
因为 volatile 关键字和 transient 关键字不能修饰方法,所以方法表的访问标志中没有 ACC_VOLATILE 和 ACC_TRANSIENT.与之相对的,synchronizes、native、strictfp 和 abstract 关键字可以修饰方法,所以方法表的访问标志中增加了 ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP 和 ACC_ABSTRACT 标志.
对于方法里的代码,经过编译器编译成字节码指令后,存放在方法属性表中一个名为「Code」的属性里面.
Java代码的方法特征签名只包括方法名称、参数顺序、参数类型. 而字节码的特征签名还包括方法返回值和受异常表
2.7 属性表集合
在 Class 文件、字段表、方法表中都可以携带自己的属性表(attribute_info)集合,用于描述某些场景专有的信息.
属性表集合不像 Class 文件中的其它数据项要求这么严格,不强制要求各属性表的顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机在运行时会略掉它不认识的属性.
在Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景的专有信息.属性表中不要求各个属性表具有严格的顺序,只要不与已有属性重名即可.下表列举了一些java虚拟机预定的属性.
表11、虚拟机属性表类型
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的格式则是完全自定义,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。属性表的的结构应满足下表中所示结构:
表12、属性表结构
2.7.1 Code属性
java中方法体在经过编译之后最终以字节的形式存放在Code属性内.Code属性出现在方法表的属性集合之中(不包括抽象类或接口的方法),Code属性表结构如下:
表13、Code属性表结构
- attribute_name_index:一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为”Code“,它代表该属性的属性名称.
- attribute_length:属性值的长度,由于属性名称索引与属性长度一共6个字节,所以属性值长度=属性表长度-6
- max_stack:代表了操作数栈(Operand Stacks)深度的最大值.在方法执行的任意时刻,操作数栈都不会超过这个最大值.虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度
- max_locals:局部变量表所需要的存储空间.单位是Slot
- code_length:字节码长度
- code:编译后生成的字节码指令
exception_table:包含4个字段(start_pc、end_pc、handler_pc、catch_type).这些字段的含义是:当字节码在start_pc行到end_pc行之间(try的范围)出现了类型为catch_type的异常或其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则跳转到handler_pc行继续处理.
2.7.2 Exceptions属性
Exception属性是和Code属性平级的一项属性,它的结构如下表所示:
表14、Exceptions表属性结构
number_of_exceptions表示方法可能抛出number_of_exceptions中受查异常,每一种受查异常用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型2.7.3 LineNumberTable属性
LineNumberTable属性用于描述java源码行号和字节码行号之间的对应关系.它并不是运行时必须的属性,但默认会生成到Class文件中,可以再javac中分别使用-g:none或-g:lines选项取消或要求生成这项信息.如果选择不生成LineNumberTable属性,当程序抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行设置断点,其结构如下表所示:
表15、LineNumberTable属性表结构
line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包含了start_pc和line_number两个u2类型的数据项,前者表示字节码行号,后者表示java源码行号2.7.4 LocalVariableTable属性
LocalVariableTable属性用于描述栈帧中局部变量表中的变量与java源码中定义的变量的关系,它也不是运行时必须数据,但默认会生成在Class文件中.可以在javac时使用-g:none或-g:lines来取消或要求生成这项信息.如果不生成这个信息,当其他人引入这个方法时,所有参数名称会丢失,IDE会使用诸如arg1、arg2之类的占位符替代原有的参数名,这对程序运行没有影响,但是对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获取参数值.其结构如下表所示:
表16、LocalVariableTable属性表结构
其中local_variable_info 项代表了一个栈帧与源码中局部变量的关联,local_variable_info表结构如下所示:
表17、local_variable_info表结构start_pc:局部变量的生命周期开始的字节码偏移量
- length:局部变量在生命周四开始的字节码的作用范围覆盖长度
- name_index:指向常量池中CONSTANT_Utf8_info型常量的索引,代表局部变量的名称
- descriptor_index:指向常量池中CONSTANT_Utf8_info型常量的索引,代表局部变量的描述符
-
2.7.5 SourceFile属性
SourceFile属性用于记录生成这个Class文件的源码名称.这个属性也是可选的,可以分别使用javac 的 -g :none或-g:source来关闭或要求生成这项信息.如果不生成这项信息,当抛出异常,堆栈中将不会显示出错代码所属的文件名.这个属性是一个定长的属性,其结构如下:
表18、SourceFile属性表结构
sourcefile_index数据项时指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件名2.7.6 ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值.只有被static修饰的变量才可以使用这个属性.
2.7.7 InnerClasses属性
InnerClasses属性用于记录内部类和宿主类之间的关系.如果一个类中定义了内部类,那编译器将会为它以及它包含的内部类生成InnerClasses属性,该属性结构如下图所示:
表19、InnerClasses属性表结构
数据项number_of_classes代表需要记录多少个内部类信息,每个内部类的信息都由一个inner_classes_info表进行描述,inner_classes_info表结构如下:
表20、inner_classes_info表结构 inner_class_info_index:指向常量池中CONSTANT_Class_info类型常量的索引,代表内部类的符号引用
- outer_class_info_index:指向常量池中CONSTANT_Class_info类型常量的索引,代表宿主类的符号引用
- inener_name_index:指向常量池中CONSTANT_Utf8_info型常量的索引,代表内部类的名称,如果是匿名内部类,那么这项值为0
- inner_class_access_flags:内部类的访问标志,它的取值范围见下表:
2.7.8 Deprecated及Synthetic属性
Deprecated及Synthetic属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值概念.Deprecated属性用于表示某个类、字段或方法,它可以通过在代码中使用@deprecated注释进行设置.Synthetic属性代表此字段或方法不是由java源码直接产生,而是由编译器自行添加的.Deprecated和Synthetic属性的结构如下:
表22、Deprecated和Synthetic属性的结构
2.7.9 StackMapTable属性
StackMapTable属性在JDK1.6发布后增加到Class文件规范中,它是一个复杂的变长属性,位于Code属性的属性表中.这个属性会在虚拟机的类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于替代以前比较消耗性能的基于数据流分析的类型推导验证器.StackMapTable属性中包含零至多个栈映射帧(Stack Map Frames),每个栈帧射帧都显式或隐式的代表了一个字节码的偏移量,用于表示执行到该字节码时局部变量表和操作数栈的验证类型.类型检查器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定指令是否符合逻辑的约束.其结构如下:
表23、StackMapTable属性表结构
2.7.10 Signature属性
Signature属性在JDK1.5发布后增加到Class文件规范中,它是一个可选的定长属性,可以出现于类、属性表和方法表结构的属性表中.在任何类、接口、初始化方法或成员的泛型简明中如果包含了类型变量()或参数化类型(),则Signature属性会为它记录泛型签名信息.Signature属性的结构如下所示:
表24、Signature属性表结构
- signature_index:必须是一个对常量池的有效索引.常量池在该索引未知的项必须是CONSTANT_Utf8_info结构,表示类签名、方法类型签名或字段类型签名. 如果当前的Signature属性是类文件的属性,则表示是类签名;如果是方法表的属性则表示是方法类型签名;如果是字段表属性则说明是字段类型签名.
2.7.11 BootstrapMethods属性
BootstrapMethods属性是在JDK1.7发布后增加到CLass文件规范中,它是一个复杂的变长属性,位于类文件表中.这个属性用于保存invokedynamic指令引用的引导方法限定符.如果某个类文件的常量池中曾经出现过CONSTANT_InvokeDynamic_info类型的常量,那么这个类文件的属性表中必须存在一个明确的BootstrapMethods属性,另外,即使CONSTANT_InvokeDynamic_info类型的常量在常量池中出现过多次,最多也只能有一个BootstrapMethods属性.BootstrapMethods属性结构如下:
表25、BootstrapMethods属性结构
其中引用到的bootstrap_method结构如下:
表26bootstrap_method结构