前言
java 文件经过编译后会产生 class 文件,这是一个存储字节码的文件,它可以被 java 虚拟机解析并运行。事实上,java 虚拟机并不是只能解析 java 语言编译产生的 class 文件,它也可以解析其他语言产生的 class 文件,事实上,java 虚拟机是一种语言无关的平台,只要 class 文件满足 java 虚拟机特定的语法和结构化约束,就能在 java 虚拟机解析运行。
总述
class 文件是一组以 8 字节为基础单位的二进制流,各个数据项目严格按照规范和大端模式(高字节存放在地址低位,低字节存放在地址高位)的顺序紧凑地排列在 class 文件中,没有任何分隔符。
class 文件中只有两种数据类型,无符号数和表,其中无符号数用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。分别以 u1、u2、u4、u8 代表 1 个字节、2 个字节、4 个字节、8个字节的无符号数。表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以 _info
结尾。
class 文件的总体数据结构如下所示,其主要的顺序是:文件类型信息 –> 文件版本号 –> 常量信息 –> 文件访问权限信息 –> 类索引 –> 父类索引 –> 接口索引信息 –> 字段表信息 –> 方法表信息 –> 属性表信息。
名称 | 描述 | 类型 | 数量 |
---|---|---|---|
magic | “魔数”,用于确认文件是否能够被 java 虚拟机解析 | u4 | 1 |
minor_version | class 文件次版本号 | u2 | 1 |
major_version | class 文件主版本号 | u2 | 1 |
constant_pool_count | 常量池中常量数量 | u2 | 1 |
constant_pool | 常量池中的常量表 | cp_info | constant_pool_count - 1 |
access_flags | 访问标志,用于标识类或接口的访问信息 | u2 | 1 |
this_class | 类索引 | u2 | 1 |
super_class | 父类索引 | u2 | 1 |
interfaces_count | 接口索引的数量 | u2 | 1 |
interfaces | 接口索引 | u2 | interfaces_count |
fields_count | 字段表数量 | u2 | 1 |
fields | 字段表 | field_info | fields_count |
methods_count | 方法表数量 | u2 | 1 |
methods | 方法表 | method_info | methods_count |
attributes_count | 属性表数量 | u2 | 1 |
attributes | 属性表 | attribute_info | attributes_count |
本文中将以下面的简单例子产生的字节码进行分析
1 | public class TestJvmClassStructure |
产生的字节码如下
1 | cafe babe 0000 0034 0013 0a00 0400 0f09 |
“魔数”
“魔数”主要是用于确认这个文件是否能够被 java 虚拟机解析的文件。class 文件中主要使用文件开始的前 4 个字节表示“魔数”,只有当值是 0xcafebabe 时,才表明这个 class 文件是能够被虚拟机解析的文件。
使用“魔数”而不是文件的后缀名进行判断主要是出于安全方面的考虑,因为文件后缀名能够随意更改。现在有很多存储文件都使用“魔数”进行文件类别的校验,如 gif。文件格式的制定者可以自由地选择魔数值,只要这个魔数值没有被广泛使用并且不会产生二义性即可。
上述例子中,前四个字节cafe babe
表明了这是一个 class 文件
版本号
跟在“魔数”后面的四个字节,即第 5 ~ 8 个字节,表明 class 文件的版本号。其中第 5、第 6 个字节表明 class 文件的次版本号,第 7、第 8 个字节表明文件的主版本号。这个版本号表明该 class 文件可以被高于或等于这个版本的虚拟机解析,使用 JDK8 编译产生的版本号是 0x00000034,表明可以被 JDK8 或高于 JDK8 以上的虚拟机解析执行。
上述例子中,第 5 ~ 8 个字节0000 0034
表明这适用于 JDK8 及以上版本的虚拟机解析运行。
常量池
常量池是跟在版本号后面的字节码。其主要存放两种数据:字面量(Literal)和符号引用(Symbolic References)。其中字面量类似于 java 语言的常量,而符号引用则属于编译原理方面的概念,主要包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
由于常量池中的常量数量不固定,因此在常量吃入口有一项 u2 类型的数据代表常量池中常量的数量(称为常量池容量)。但是有一点需要注意,其技术从 1 开始而不是从 0 开始,这就意味者如果常量池容量为 10,则常量的数量为 10 - 1 = 9。class 文件结构中只有常量池的容量计数是从 1 开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都是从 0 开始的。
常量池中共有 14 中常量,其结构如下所示
常量 | 项目 | 类型 | 描述 |
---|---|---|---|
CONSTANT_Utf8_info | 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 值 | |
CONSTNAT_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_Method- ref_info |
tag | u1 | 区分常量类型,值为 10 |
index | u2 | 指向声明方法的类描述符 CONSTANT_Class_info 的索引项 | |
index | u2 | 指向名称及类型描述符 CONSTANT_NameAndType 的索引项 | |
CONSTANT_Interface- Methodref_info |
tag | u1 | 区分常量类型,值为 11 |
index | u2 | 指向声明方法的类描述符 CONSTANT_Class_info 的索引项 | |
index | u2 | 指向名称及类型描述符 CONSTANT_NameAndType 的索引项 | |
CONSTANT_Name- AndType_info |
tag | u1 | 区分常量类型,值为 12 |
index | u2 | 指向字段或方法名称常量项的索引 | |
index | u2 | 指向字段或方法描述符常量项的索引 | |
CONSTANT_Method- Handle_info |
tag | u1 | 区分常量类型,值为 15 |
reference_kind | u1 | 值必须在 1 ~9 之间(包括 1 和 9),它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为 | |
reference_index | u2 | 值必须是对常量池的有效索引 | |
CONSTANT_Method- Type_info |
tag | u1 | 区分常量类型,值为 16 |
reference_kind | u1 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示方法的描述符 | |
CONSTANT_Invoke- Dynamic_info |
tag | u1 | 区分常量类型,值为 18 |
bootstrap_me- thod_attr_index |
u2 | 值必须是对当前 Class 文件中引导方法表的 bootstrap_methods[] 数组的有效索引 | |
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处必须是 CONSTANT_NameAndType_info 结构,表示方法名和方法描述符 |
上述例子中,第 9 ~ 181 个字节表示的是常量池中的常量内容,具体表示含义如下
1 | 00 13 常量池中的数量,值是19,说明有18个常量项 |
执行 javap 命令对 class 文件进行反编译,可以看到其常量池如下所示,与上述字节码的解析相同
1 | Constant pool: |
文件访问标志
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个 class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。具体的标志位及其标志的含义如下所示
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为 public 类型,如果是,取值为 0x0001,否则为 0x0000 |
ACC_FINAL | 0x0010 | 是否被声明为 final,如果是,取值为 0x0010,否则为 0x0000,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial字节码指令的新语义, invokespecial 指令的语义在 JDK 1.0.2 发生过改变,为了区别这条指令使用哪种语义, JDK 1.0.2 之后编译出来的类的这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 是否是一个接口,如果是,取值为 0x0200,否则为 0x0000 |
ACC_ABSTARCT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,此值为 0x0400,其他类值为 0x0000 |
ACC_SYNTHETIC | 0x1000 | 该 class 文件是否非用户代码产生,如果是,取值为 0x1000,否则为 0x0000 |
ACC_ANNOTATION | 0x2000 | 是否是一个注解,如果是,取值为 0x2000,否则为 0x0000 |
ACC_ENUM | 0x4000 | 是否是一个枚举,如果是,取值为 0x4000,否则为 0x0000 |
对于这些标志位,可能只会用到一部分,对于用到的标志位,将其标志值进行或运算得到最终的标志值,对于没用到的标志位,其值不参与运算(或者说其值为 0)。
对于上述的例子,第 182 ~ 183 个字节是00 21
,是由 0x0001 和 0x0020 或运算得到的,所以ACC_PUBLIC、ACC_SUPER为真,ACC_FINAL、ACC_INFERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM为假,即说明这是一个非 final 的被 public 修饰的类。
类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据的集合。class 文件中由这三项数据来确定这个类的继承关系,其中类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,接口索引集合用于描述这个类实现的接口。
类索引和父类索引都指向一个类型为 CONSTANT_Class_info 的类描述符常量,通过 CONSTANT_Class_info 类型的常量中的索引值可以找到定义在 CONSTANT_Utf8_info 类型的常量中的全限定名字符串。
上述例子中,第 184 ~ 187 个字节表示类索引和父类索,引对应的值如下
1 | 00 03 本类索引,this_class,对应到第 3 个常量项,得知指向第 17 个常量项,值是TestJvmClassStructure |
对于接口索引集合,首先用一个 u2 的数据类型的数据来表示接口的数量 n ,紧接着跟着 n 个 u2 数据类型的接口索引,跟类索引和父类索引一样,也是指向一个类型为 CONSTANT_Class_info 的类描述符常量,通过 CONSTANT_Class_info 类型的常量中的索引值可以找到定义在 CONSTANT_Utf8_info 类型的常量中的全限定名字符串。然而,如果表示接口数量的 u2 数据类型的数据为 0,则后面接口的索引表不占任何字节,即后面直接跟着字段表集合。
上述例子中,第 188 ~ 189 个字节表示接口索引信息,由于没有接口,故接口数量为 0,其字节码如下
1 | 00 00 接口索引集合大小,值是0,表示没有实现接口 |
字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但是不包括在方法内部声明的局部变量。字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能列出原本 java 代码中不存在的字段,比如在内部类中为了保持对外部类的访问性,为自动添加指向外部类实例的字段。
字段表的结构如下所示
名称 | 类型 | 数量 | 描述 |
---|---|---|---|
access_flags | u2 | 1 | 字段修饰符,用于判断字段的访问权限等信息 |
name_index | u2 | 1 | 对常量池中常量的引用,表示字段的简单名称 |
descriptor_index | u2 | 1 | 对常量池中常量的引用,表示字段和方法的描述符 |
attributes_count | u2 | 1 | 属性表中属性的数量 |
attributes | attribute_info | attributes_count | 属性表中属性的信息 |
其中 access_flags 与文件访问标志类似,其定义的标志名称的值在该标志位为真时取对应的值,为假时取 0,最终将所有标志位进行与运算得到最后结果。其具体项及其含义如下所示
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否为 public 类型,如果是,取值为 0x0001,否则为 0x0000 |
ACC_PRIVATE | 0x0002 | 字段是否是 private 类型,如果是,取值为 0x0002,否则为 0x0000 |
ACC_PROTECTED | 0x0004 | 字段是否是 protected 类型,如果是,取值为 0x0004,否则为 0x0000 |
ACC_STATIC | 0x0008 | 字段是否是 static 类型,如果是,取值为 0x0008,否则为 0x0000 |
ACC_FINAL | 0x0010 | 字段是否是 final 类型,如果是,取值为 0x0010,否则为 0x0000 |
ACC_VOLATILE | 0x0040 | 字段是否是 volatile 类型,如果是,取值为 0x0040,否则为 0x0000 |
ACC_TRANSIENT | 0x0080 | 字段是否是transient 类型,如果是,取值为 0x0080,否则为 0x0000 |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生,如果是,取值为 0x1000,否则为 0x0000 |
ACC_ENUM | 0x4000 | 字段是否是 enum 类型,如果是,取值为 0x4000,否则为 0x0000 |
上述例子中,第 190 ~ 199 个字节表示字段表信息,其具体含义如下
1 | 00 01 字段表中字段的数量,值是1 |
方法表集合
方法表的结构与字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。其含义如下所示
名称 | 类型 | 数量 | 描述 |
---|---|---|---|
access_flags | u2 | 1 | 方法修饰符,用于判断方法的访问权限等信息 |
name_index | u2 | 1 | 对常量池中常量的引用,表示方法的简单名称 |
descriptor_index | u2 | 1 | 对常量池中常量的引用,表示字段和方法的描述符 |
attributes_count | u2 | 1 | 属性表中属性的数量 |
attributes | attribute_info | attributes_count | 属性表中属性的信息 |
其中 access_flags 与文件访问标志类似,其定义的标志名称的值在该标志位为真时取对应的值,为假时取 0,最终将所有标志位进行与运算得到最后结果。其具体项及其含义如下所示
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否为 public 类型,如果是,取值为 0x0001,否则为 0x0000 |
ACC_PRIVATE | 0x0002 | 方法是否是 private 类型,如果是,取值为 0x0002,否则为 0x0000 |
ACC_PROTECTED | 0x0004 | 方法是否是 protected 类型,如果是,取值为 0x0004,否则为 0x0000 |
ACC_STATIC | 0x0008 | 方法是否是 static 类型,如果是,取值为 0x0008,否则为 0x0000 |
ACC_FINAL | 0x0010 | 方法是否是 final 类型,如果是,取值为 0x0010,否则为 0x0000 |
ACC_SYNCHRONIZED | 0x0020 | 方法是否是 synchronized 类型,如果是,取值为 0x0020,否则为 0x0000 |
ACC_BRIDGE | 0x0040 | 方法是否由编译器产生的桥接方法,如果是,取值为 0x0040,否则为 0x0000 |
ACC_VARAGS | 0x0080 | 方法是否接收不定参数,如果是,取值为 0x0080,否则为 0x0000 |
ACC_NATIVE | 0x0100 | 方法是否是 native 类型,如果是,取值为 0x0100,否则为 0x0000 |
ACC_ABSTRACT | 0x0400 | 方法是否是 abstract 类型,如果是,取值为 0x0400,否则为 0x0000 |
ACC_STRICTFP | 0x0800 | 方法是否是 strictfp 类型,如果是,取值为 0x0800,否则为 0x0000 |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动产生,如果是,取值为 0x1000,否则为 0x0000 |
需要注意的是,虽然实例构造函数 <init>
和类构造函数 <clinit>
也可以由编译器自动生成,但是,这两个函数的 ACC_SYNTHETIC 仍然为假。另外,如果父类方法在子类中没有被重写(override),方法表集合中就不会出现来自父类的方法信息。
上述例子中,第 200 ~ 289 个字节表示方法表的信息,其具体含义如下
1 | 00 02 方法表中方法的数量,值是2 |
属性表集合
属性表是比较“自由”的一种数据结构,其不同于字段表或方法表等有严格的顺序限制,其可以自由地定义顺序,并且只要不与已有属性名重复,任何编译器都可以向属性表中写入自己定义的独特的不同于其他编译器的属性信息,在解析时,java 虚拟机会自动忽略其不认识的属性。因此,不同的厂商实现的编译器的属性表会有所不同。
为了能够正确解析 class 文件,java 虚拟机规范中预定义了一些属性,其一部分属性及其含义如下所示
属性名称 | 使用位置 | 含义 |
---|---|---|
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 语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameerized Types),则 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 属性作用刚好相反,用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameter- Annotaions |
方法表 | JDK 1.5 中新增的属性,与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法参数 |
RuntimeInvisibleParameter- Annotaions |
方法表 | JDK 1.5 中新增的属性,与 RuntimeInvisibleAnnotations属性类似,只不过作用对象为方法参数 |
AnnotationDefault | 方法表 | JDK 1.5 中新增的属性,用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | JDK 1.7 中新增的属性,用于保存 invokedynamic 指令引用的引导方法限定符 |
对于每个属性,它的名称需要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,而属性值的结构则是自己定义的,值需要通过一个 u4 的长度属性说明属性值占用的位数即可。属性表的结构如下
名称 | 类型 | 数量 | 描述 |
---|---|---|---|
attribute_name_index | u2 | 1 | 对常量池中常量的引用,表示属性类型名称 |
attribute_length | u4 | 1 | 该类型属性所占的字节码长度 |
info | u1 | attribute_length | 该类型属性的具体字节码 |
Code 属性
Code属性主要是用于存放 java 方法体的相关内容的字节码信息。其结构如下所示
名称 | 类型 | 数量 | 描述 |
---|---|---|---|
attribute_name_index | u2 | 1 | 属性名称,指向 CONSTANT_Utf8_info 的常量索引,固定是 “Code” |
attribute_length | u4 | 1 | 属性内容信息占有的字节码长度 |
max_stack | u2 | 1 | 操作数栈深度的最大值。在方法执行的任何时候操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧中的操作栈深度 |
max_locals | u2 | 1 | 局部变量表所需的存储空间,单位是 Solt。Slot 是虚拟机为局部变量分配内存所使用的最小单位。对于 byte、char、float、int、short、boolean 和 retruanAddress 等长度不超过 32 位的数据类型,用 1 个 Slot 存放。对于 double 和 long 这两种 64 位的数据类型则需要 2 个Slot。 |
code_length | u4 | 1 | 字节码指令长度 |
code | u1 | code_length | 字节码指令 |
exception_table_length | u2 | 1 | 显示异常处理表长度,用于表示方法体内的异常 |
exception_table | exception_info | exception_table_length | 显示异常处理表信息 |
attributes_count | u2 | 1 | 属性表中属性数量 |
attributes | attribute_info | attributes_count | 属性表中属性信息 |
其中异常表(exception_table)的数据结构如下,其表示在 start_pc 到 end_pc (不包括 end_pc 本身)之间出现了类型为 catch_type 或者子类的异常,则跳转到 handle_pc 处继续执行。当 catch_type 值是 0 时,代表任意异常情况都需要转向 handler_pc 处进行处理。
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
start_pc | u2 | 1 | 可能发生异常的开始的地方 |
end_pc | u2 | 1 | 被包裹的异常块的结束位置 |
handler_pc | u2 | 1 | 处理异常的入口处 |
catch_type | u2 | 1 | 指向一个 CONSTANT_Class_info 型常量的索引,表示异常的类型,如果为 0,表示接受任何类型 |
Exceptions 属性
Exceptions 属性的作用是列举出方法中可能抛出的受检异常(Checked Exceptions),也就是方法描述是在 throws 关键字后面列举的异常。不同于 Code 属性中的异常,Code 属性中的异常表示的是方法体内的 try-catch块。Exceptions 属性的结构如下所示
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
attribute_name_index | u2 | 1 | 属性名称,指向 CONSTANT_Utf8_info 的常量索引,固定是 “Exceptions” |
attribute_length | u4 | 1 | 属性内容信息占有的字节码长度 |
number_of_exceptions | u2 | 1 | 方法抛出的受检异常的数量 |
exception_index_table | u2 | number_of_exceptions | 一个指向常量池中的 CONSTANT_Class_info 型常量的索引,表示受检异常的类型 |
LineNumberTable 属性
LineNumberTable 属性用于描述 java 源码行号与字节码行号(字节码的偏移量)之间的对应关系。这并不是运行时的必需属性,但是默认为生成到 class 文件中,可以在 javac 中分别使用 -g:none 选项来取消生成这项信息,也可以使用 -g:lines 选项来生成这项信息。其结构如下所示
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
attribute_name_index | u2 | 1 | 属性名称,指向 CONSTANT_Utf8_info 的常量索引,固定是 “LineNumberTable” |
attribute_length | u4 | 1 | 属性内容信息占有的字节码长度 |
line_number_table_length | u2 | 1 | line_number_table 项的数量 |
line_number_table | line_number_info | line_number_table_length | 字节码与 java 源码行号对应关系 |
其中 line_number_table 的数据结构如下所示
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
start_pc | u2 | 1 | 字节码的偏移量 |
line_number | u2 | 1 | java 源码行号 |
LocalVariableTable 属性
LocalVariableTable 属性用于描述栈帧中局部变量表中变量与 java 源码中定义的变量之间的关系,这不是运行时必需的属性,但是默认会生成到 class 文件中。可以在 javac 命令中使用 -g:none 选项来取消生成这项信息,也可以使用 -g:vars 选项要求生成这项信息。其结构如下所示
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
attribute_name_index | u2 | 1 | 属性名称,指向 CONSTANT_Utf8_info 的常量索引,固定是 “LocalVariableTable” |
attribute_length | u4 | 1 | 属性内容信息占有的字节码长度 |
local_variable_table- _length |
u2 | 1 | 局部变量表数量 |
local_variable_table | local_variable_info | local_variable_table- _length |
局部变量表信息 |
其中 local_variable_table 的结构如下所示
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
start_pc | u2 | 1 | 局部变量的生命周期开始的字节码偏移量 |
length | u2 | 1 | 局部变量的生命周期的作用范围的长度 |
name_index | u2 | 1 | 指向常量池中 CONSTANT_Utf8_info 型常量的索引,表示局部变量的名称 |
descriptor_index | u2 | 1 | 指向常量池中 CONSTANT_Utf8_info 型常量的索引,表示局部变量的描述符 |
index | u2 | 1 | 表示局部变量在栈帧局部变量表中 Slot 的位置。当这个变量的数据类型是 64 位类型(double 和 long)时,它占用的 Slot 为 index 和 index + 1 两个 |
SourceFile 属性
SourceFile 属性用于记录生成这个 class 文件的源码文件名称,这是一个可选的属性,默认情况下会生成。可以在javac 命令中使用 -g:none 选项来取消生成这项信息,也可以使用 -g:source 选项明确要求生成这项信息。其结构如下图所示
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
attribute_name_index | u2 | 1 | 属性名称,指向 CONSTANT_Utf8_info 的常量索引,固定是 “SourceFile” |
attribute_length | u4 | 1 | 属性内容信息占有的字节码长度 |
source_index | u2 | 1 | 指向常量池中 CONSTANT_Utf8_info 型常量的索引,表示源码文件的文件名 |
上述例子中,第 290 ~ 299 个字节表示 SourceFile 属性信息,其具体含义如下
1 | 00 01 属性数量,值是1 |
ConstantValue 属性
ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值,只有被 static 关键字修改时的变量(类变量)才可以使用这个属性。其结构如下所示
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
attribute_name_index | u2 | 1 | 属性名称,指向 CONSTANT_Utf8_info 的常量索引,固定是 “ConstantValue” |
attribute_length | u4 | 1 | 属性内容信息占有的字节码长度 |
constantvalue_index | u2 | 1 | 指向常量池中字面常量的引用。根据字段类型的不同,字面量可以是 CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_String_info 常量中的一种 |
InnerClasses 属性
InnerClasses 属性用于记录内部类与宿主类之间的关联,其结构如下所示
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
attribute_name_index | u2 | 1 | 属性名称,指向 CONSTANT_Utf8_info 的常量索引,固定是 “InnerClasses” |
attribute_length | u4 | 1 | 属性内容信息占有的字节码长度 |
number_of_classes | u2 | 1 | 需要记录的内部类的数量 |
inner_classes | inner_classes_info | number_of_classes | 需要记录的内部类的信息 |
其中 inner_classes 的结构如下所示
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
inner_class_info_index | u2 | 1 | 指向常量池中 CONSTANT_Class_info 型常量的索引,表示内部类的符号引用 |
outer_class_info_index | u2 | 1 | 指向常量池中 CONSTANT_Class_info 型常量的索引,表示宿主类的符号引用 |
inner_name_index | u2 | 1 | 指向常量池中 CONSTANT_Utf8_info 型常量的索引,表示内部类的名称,如果是匿名内部类,则值是 0 |
inner_class_access_flags | u2 | 1 | 内部类的访问标志 |
其中 inner_class_access_flags 文件访问标志类似,其定义的标志名称的值在该标志位为真时取对应的值,为假时取 0,最终将所有标志位进行与运算得到最后结果。其具体项及其含义如下所示
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 内部类是否为 public 类型,如果是,取值为 0x0001,否则为 0x0000 |
ACC_PRIVATE | 0x0002 | 内部类是否是 private 类型,如果是,取值为 0x0002,否则为 0x0000 |
ACC_PROTECTED | 0x0004 | 内部类是否是 protected 类型,如果是,取值为 0x0004,否则为 0x0000 |
ACC_STATIC | 0x0008 | 内部类是否是 static 类型,如果是,取值为 0x0008,否则为 0x0000 |
ACC_FINAL | 0x0010 | 内部类是否是 final 类型,如果是,取值为 0x0010,否则为 0x0000 |
ACC_INTERFACE | 0x0020 | 内部类是否是接口,如果是,取值为 0x0020,否则为 0x0000 |
ACC_ABSTRACT | 0x0400 | 内部类是否是 abstract 类型,如果是,取值为 0x0400,否则为 0x0000 |
ACC_SYNTHETIC | 0x1000 | 内部类是否由编译器自动产生,如果是,取值为 0x1000,否则为 0x0000 |
ACC_ANNOTATION | 0x2000 | 内部类是否是一个注解,如果是,取值为 0x2000,否则为 0x0000 |
ACC_ENUM | 0x4000 | 内部类是否是一个枚举类型,如果是,取值为 0x4000,否则为 0x0000 |
Deprecated 属性
Deprecated 属性用于表示某个类、字段或者方法是否已经被程序作者标注为不推荐使用,对应 java 代码中的 @deprecated
注解。这是一个布尔属性,只存在有和没有的区别,没有属性值的概念。也就是说,要么在 class 文件中 没有这个属性,要么存在这个属性,但是属性长度为 0。其结构如下
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
attribute_name_index | u2 | 1 | 属性名称,指向 CONSTANT_Utf8_info 的常量索引,固定是 “Deprecated” |
attribute_length | u4 | 1 | 值为固定值,0x00000000 |
Synthetic 属性
Synthetic 属性代表此字段或者方法并不是由 java 源码直接产生的,而是由编译器自行添加的。需要注意的是,实例构造器 <init>
和类构造器 <clinit>
虽然也可以由编译器自动生成,但是此时值为假。与 Deprecated 属性一样,这也是一个布尔属性。其结构如下所示
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
attribute_name_index | u2 | 1 | 属性名称,指向 CONSTANT_Utf8_info 的常量索引,固定是 “Synthetic” |
attribute_length | u4 | 1 | 值为固定值,0x00000000 |
Signature 属性
Signature 属性主要用于记录泛型签名信息,其结构如下所示
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
attribute_name_index | u2 | 1 | 属性名称,指向 CONSTANT_Utf8_info 的常量索引,固定是 “Signature” |
attribute_length | u4 | 1 | 属性内容信息占有的字节码长度 |
signature_index | u2 | 1 | 指向常量池中 CONSTANT_Utf8_info 型常量的索引,表示类签名、方法类型签名或字段类型签名 |
BootstrapMethods 属性
BootstrapMethods 属性用于保存 invokedynamic 指令引用的引导方法限定符。如果某个类文件结构的常量池中曾经出现过 CONSTANT_InvokeDynamic_info 类型的常量,那么这个类文件的属性表中必须存在一个明确的 BootstrapMethods 属性,另外,即使 CONSTANT_InvokeDynamic_info 类型的常量在常量池中出现过多次,类文件的属性表中最多也只有一个 BootstrapMethods 属性。其结构如下所示
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
attribute_name_index | u2 | 1 | 属性名称,指向 CONSTANT_Utf8_info 的常量索引,固定是 “BootstrapMethods” |
attribute_length | u4 | 1 | 属性内容信息占有的字节码长度 |
num_bootstrap_methods | u2 | 1 | 引导方法限定符的数量 |
bootstrap_methods | bootstrap_method | num_bootstrap_methods | 引导方法信息 |
其中 bootstrap_methods 结构如下
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
bootstrap_method_ref | u2 | 1 | 指向常量池中 CONSTANT_MethodHandle_info 型常量的索引 |
num_bootstrap_arg- uments |
u2 | 1 | bootstrap_arguments[] 数组成员的数量 |
bootstrap_arguments | u2 | num_bootstrap_arg- uments |
指向常量池中字面常量的引用。值必须是 CONSTANT_String_info、CONSTANT_Class_info、CONSTANT_Integer_info、 CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_MethodHandle_info、CONSTANT_MethodType_info 常量中的一种 |
附录
例子完整字节码解析
1 | ca fe ba be 格式,固定值 |
例子完整 javap 反编译输出
1 | Classfile /C:/Users/zhchun/Desktop/TestJvmClassStructure.class |
参考资料
[1] 周志明. 深入理解Java虚拟机:JVM高级特性与最佳实践[M]. 北京:机械工业出版社,2013