深入JVM之手撕Class文件结构

135 阅读45分钟

前言

Java同学应该都听说过Java有一个非常著名的宣传口号“Write Once,Run Anywhere”,一次编译,到处运行,也即一份Class文件可以在不同的操作系统上运行,这都得力于Java虚拟机和字节码文件。

Java虚拟机不与包括Java语言在内的任何编程语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及其它辅助信息。任意一门编程语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件,以Class文件作为他们产品的交付媒介。

Java技术能够一直保持着非常良好的向后兼容性,Class文件结构的稳定性功不可没,Class文件结构的内容在第一版《Java虚拟机规范》中就已经定义好了,Java从1997年,发展至今,经历了十余个大版本,无数个小版本的更新迭代,但是Class文件格式几乎没有出现任何改变,只是在原有结构的基础上新增内容,扩充功能。

Class文件结构

Class文件是一组以8个字节为基础单位的二进制流,各个数据项严格按照顺序紧凑的排列在文件之中,中间没有任何分隔符,当遇到需要占用8个字节以上空间的数据项时,会按照高位在前的方式分割成若干个8字节进行存储。

Class文件采用了一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型,无符号数和表:

  • 无符号数:无符号数属于基本数据类型,以u1u2u4u8来分别表示1248个字节的无符号数,可以用来描述数字、索引、数量值或者UTF-8编码的字符串。
  • 表:表是由多个无符号数或者其它表作为数据项构成的复合数据类型,所有表的命名都以_info结尾,用于描述多层次关系复合结构的数据。

整个Class文件本质上可以视为是一张表,所有数据项严格按照顺序排列构成,如下表所示:

类型名称字节数数量描述
u4magic41魔数,用来标识Class文件
u2minor_version21次版本号
u2major_version21主版本号
u2constant_pool_count21常量池计数,从1开始
cp_infoconstant_poolnconstant_pool_count - 1常量池
u2access_flags21访问标志
u2this_class21类索引
u2super_class21父类索引
u2interfaces_count21接口计数值
u2interfaces2interfaces_count接口索引集合
u2fields_count21字段计数值
field_infofieldsnfields_count字段集合
u2methods_count21方法计数值
method_infomethodsnmethods_count方法集合
u2attributes_count21属性计数值
attribute_infoattributesnattributes_count属性集合

下面我们将按照上述表中所列的数据项,深入分析其作用及底层结构。

示例程序

后续内容的讲解,将都以这段程序输出的Class文件作为基础来进行讲解

public class TestClass {

    private int m;

    public int inc() {
        return m + 1;
    }
}

idea可安装BinEd插件以十六进制显示编译后的Class文件,mac同学也可通过xxd -g 1 -u [文件名称]命令将字节码内容转为十六进制便于查看,如下图所示: image.png

魔数

在我们将Class文件转换成十六进制显示后,可以清楚的看到文件的开头有一串特殊的字符CAFEBABY,占4个字节,称为魔数(Magic Numer),用来确定这个文件是否是一个合法的Class文件。 image.png

版本号

紧挨着魔数的4个字节存储的是Class文件的次版本号和主版本号,第5和第6字节的值为0x0000(十进制0),第7和第8字节的值为0x0034(十进制52),也即对应的主版本号为52,次版本号为0。 image.png

JDK1.0-1.1使用了45.0-45.3JDK1.246,之后每升级一个大版本就加1,1.2之后大版本号可通过(主版本号-44)即可得到JDK所处版本,例如本例是基于JDK8,因此主版本号是52

常量池

常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目。

由于常量池中常量的数量是不固定的,因此需要有个地方存储常量池中常量的个数,紧挨着主版本号的有一项u2类型的数据,代表常量池容量计数值(constant_pool_count),从1开始,正常都是从0开始计数,而此处设计者将0空出来了,这是为了后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以将索引值设置为0来表示。

对比着我们的Class文件查看第9和第10个字节值为0x0016,即十进制的22,这就代表常量池中有21项常量(因为从1开始计数,所以需要-1),从1~21。 image.png

常量池中主要存放两大类常量:

  • 字面量:字面量是不变的数据,主要包括数值(如整数、浮点数)和字符串字面量。例如,一个整数100或一个字符串"Hello World",在源代码中直接赋值,编译后存储在常量池中
  • 符号引用:符号引用是对类、接口、字段、方法等的引用,它们不是由字面量值给出的,而是通过符号名称(如类名、方法名)和其他额外信息(如类型、签名)来表示。这些引用在类文件中以一种抽象的方式存在,它们在类加载时被虚拟机解析为具体的内存地址。

常量池中每一项常量都是一个表,总共有17种,每个表结构的第一位是个u1类型的标志位(tag),代表着当前常量属于哪种类型,17种常量类型如下表所示:

常量 项目 类型 描述
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值
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_Mehtodref_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_Dynamic_info tag u1 值为17
bootstrap_method_attr_index u2 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_type_index u2 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符
CONSTANT_InvokeDynamic_info tag u1 值为18
bootstrap_method_attr_index u2 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_type_index u2 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符
CONSTANT_Module_info tag u1 值为19
name_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示模块的名字
CONSTANT_Package_info tag u1 值为20
name_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示包的名字

接着我们照着转换为16进制后的字节码内容来分析下常量池中的第一项常量。常量池的第一项常量,它的标志位是0x0A,十进制即10,根据上表得知这个常量是CONSTANT_Mehtodref_info类型,此类型代表一个类中方法的符号引用。 image.png

通过上表我们能知道CONSTANT_Mehtodref_info类型首位是tag标志位,用于区分常量类型,紧接着有两个index,是常量池中的索引值,分别是指向常量池中CONSTANT_Class_info类型的常量和CONSTANT_NameAndType类型的常量。索引值分别的是0x0004(十进制4)和0x0012(十进制18)。 image.png

为了便于观察,我们使用JDK自带的反编译工具,通过控制台查看字节码文件的内容,输入javap -v class文件名称将会反编译class文件并输出到控制台: image.png

从反编译后的结果中可以看到,主次版本号和常量池个数都与我们计算的结果相同,并且第1项常量也与我们手工计算的结果一致。

访问标记

在常量池后面紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口、是否定义为public类型、是否定义为abstract类型、如果是类的话,是否被声明为final等等,具体的访问标志见下表。

标志名称标志值含义
ACC_PUBLIC0x0001是否为public类型
ACC_FINAL0x0010是否被声明为final,只有类可以设置
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义在JDK1.0.2发生过改变,为了区别这条指令使用哪种语义,JDK1.0.2之后编译出来的类的这个标志都必须为真
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或抽象类来说,此标志值为真,其它类型值为假
ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举
ACC_MODULE0x8000标识这是一个模块

我们的示例代码TestClass是一个普通的Java类,只被public修饰,并在JDK8上编译出来的,因此它有两个访问标志,ACC_PUBLICACC_SUPERaccess_flags的值为0x0001|0x0020=0x0021,对应的十六进制字节码位置为: image.png

在我们反编译后的字节码中也有体现: image.png

类索引、父类索引与接口索引集合

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类的继承关系。

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类全限定名,由于Java语言不允许多继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object之外,所有Java类的父类索引都不为0。

接口索引集合就是用来描述这个类实现了哪些接口,这个被实现的接口将按照implements关键字后的接口顺序从左到右排列在接口索引集合中(如果Class文件是一个接口,则是extends关键字后的接口)。

类索引、父类索引都按顺序排列在访问标志之后,它们都各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型常量中的索引值可以找到常量池中类型为CONSTANT_Utf8_info常量中的全限定名字符串。

以示例代码为例,类索引和父类索引十六进制分别的是0x0003(十进制3)、0x0004(十进制4),对应常量池中索引为3和4的常量: image.png

索引为3和4的CONSTANT_Class_info类型常量中的索引值又分别指向到了索引为20和21的常量: image.png

最终得到类的全限定名为com/cchangy/jvm/TestClass,父类全限定名为java/lang/Object,全限定名仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”号表示全限定名结束。

父类索引后面是u2类型的接口计数值(interfaces_count),表示接口的个数,如果该类没有实现任何接口,则该计数值为0,后面的接口索引表不在占用任何字节。示例代码没有实现任何接口,因此值为0: image.png

接着我们来改动下代码,实现一个自定义的接口,在来看下转为十六进制后此处的值是多少,可以看到值变为了0x0001(十进制1): image.png

紧接着接口计数值的是u2类型的接口索引集合,我们实现了一个接口,因此会有一个接口索引,十六进制为0x0005(十进制5): image.png

对应常量池索引为5的CONSTANT_Class_info类型常量,常量中的索引值又指向到了索引为23的CONSTANT_Utf8_info类型常量,得到了接口的全限定名字符串com/cchangy/jvm/TestInterfaceimage.png

字段表集合

紧跟着接口索引集合的是u2类型的字段计数值fileds_count,用来记录字段数量。接着就是field_info类型的字段表集合(fields),用于描述接口或类中声明的变量,包括类级别变量和实例级别变量,但是不包括方法内部声明的局部变量。

字段表的结构如下表所示:

类型名称数量描述
u2access_flags1字段访问标志
u2name_index1字段简单名称索引值
u2descriptor_index1字段描述符索引值
u2attributes_count1属性计数值
attribute_infoattributesattribute_count属性表集合

每个字段结构的首位为访问标志位access_flags,与类的access_flags非常相似,都是一个u2类型的数据,只是可设置的标志位不同,字段支持的访问标志如下表所示:

标志名称标志值含义
ACC_PUBLIC0x0001字段是否public
ACC_PRIVATE0x0002字段是否private
ACC_PROTECTED0x0004字段是否protected
ACC_STATIC0x0008字段是否static
ACC_FINAL0x0010字段是否final
ACC_VOLATILE0x0040字段是否volatile
ACC_TRANSIENT0x0080字段是否transient
ACC_SYNTHETIC0x1000字段是否由编译器自动产生
ACC_ENUM0x4000字段是否enum

跟随访问标志的是两个索引值:字段简单名称索引值(name_index)和字段描述符索引值(descriptor_index),它们都是对常量池中数据项的引用。

简单名称就是指没有类型和参数修饰的方法或字段名称,我们示例代码中的inc()方法和m字段的简单名称分别就是“inc”“m”

相比简单名称,字段描述符就要复杂一些,描述符的作用是用来描述字段的数据类型、方法的参数列表(包括参数数量、类型、顺序)和返回值。根据描述符规则,基本数据类型(byte、short、int、long、double、float、boolean、char)以及代表无返回值的void类型都用一个大小字符来表示,而对象类型则用字符“L” 加对象的全限定名来表示。各描述符含义见下表所示:

标识字符含义
B基本类型byte
C基本类型char
D基本类型double
F基本类型float
I基本类型int
J基本类型long
S基本类型short
Z基本类型boolean
V特殊类型void
L对象类型,如Ljava/lang/Object;

对于数组类型,每一维度将使用一个“[”字符来描述,例如一个定义为“new String[][]”类型的二维数组则会被记录成“[[Ljava.lang.String;”,一个整形数组“int[]”会被记录成“[I”

方法表集合

字段表后面就是u2类型的方法计数值(methods_count),用来记录方法数量,接着就是method_info类型的方法表集合(methods),用来描述接口或类钟定义的方法。方法表和字段表的结构可以说是一模一样,数据项含义也非常相似,仅在访问标志和属性表集合的可选项有些区别。方法表的结构如下表所示:

类型名称数量描述
u2access_flags1方法访问标志
u2name_index1方法简单名称索引值
u2descriptor_index1方法的描述符索引值
u2attributes_count1属性计数值
attribute_infoattributesattribute_count属性表集合

方法的访问标志如下表所示:

标志名称标志值含义
ACC_PUBLIC0x0001方法是否为public
ACC_PRIVATE0x0002方法是否为private
ACC_PROTECTED0x0004方法是否为protected
ACC_STATIC0x0008方法是否为static
ACC_FINAL0x0010方法是否为final
ACC_SYNCHRONIZED0x0020方法是否为synchronized
ACC_BRIDGE0x0040方法是不是由编译器产生的桥接方法
ACC_VARARGS0x0080方法是否接受不定参数
ACC_NATIVE0x0100方法是否为native
ACC_ABSTRACT0x0400方法是否为abstract
ACC_STRICT0x0800方法是否为strictfp
ACC_SYNTHETIC0x1000方法是否由编译器自动产生

描述符在描述方法时,按照先参数,后返回值的顺序描述,参数列表严格按照参数的顺序排列在小括号“()”内,返回值类型描述在右括号外。如方法void inc()的描述符为“()V”,方法String toString()的描述符为“()Ljava.lang.String;”,方法int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述符为([CII[CIII)I

属性表集合

在前面讲解字段表和方法表结构的时候,都多次见到过属性表,那么属性表到底是用来干嘛的呢,下面跟着我一起来揭密。

紧跟着方法表的是一项u2类型的属性计数值(attributes_count),用来记录属性数量,接着就是attribute_info类型的属性表集合(attributes)。属性表与Class文件中其它的数据项不同,没有严格要求其顺序、长度、内容,允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自定义的属性信息,Java虚拟机在运行时会忽略掉不认识的属性。《Java虚拟机规范》最初只定义了9项所有Java虚拟机实现都应当能识别的属性,如今最新的规范中预定义属性已增加到29项,具体属性含义见下表:

属性名称使用位置含义
Code方法表java代码编译成的字节码指令
ConstantValue字段表由final关键字定义的常量值
Deprecated类、方法表、字段表被声明deprecated的类、方法、字段
Exceptions方法表方法抛出的异常列表
EnclosingMethod类文件仅当一个类为局部类或匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClasses类文件内部类列表
LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量描述
StackMapTableCode属性JDK6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature类、方法表、字段表JDK5中新增的属性,用于支持泛型情况下的方法签名。在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息
SourceFile类文件记录源文件名称
SourceDebugExtension类文件JDK5中新增的属性,用于存储额外的调试信息。例如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR45提案为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用该属性就可以用于存储这个标准所新加入的调试信息
Synthetic类、方法表、字段表标识方法或字段为编译器自动生成的
LocalVariableTypeTableJDK5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations类、方法表、字段表JDK5中新增的属性,为动态注解提供支持,该属性用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的
RuntimeInvisibleAnnotations类、方法表、字段表JDK5中新增的属性,与RuntimeVisibleAnnotations属性作用刚好相反,用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotations方法表JDK5中新增的属性,作用与RuntimeVisibleAnnotations属性类似,只不过作用对象方法参数
RuntimeInvisibleParameterAnnotations方法表JDK5中新增的属性,作用与RuntimeInvisibleAnnotations属性类似,只不过作用对象方法参数
AnnotationDefault方法表JDK5中新增的属性,用于记录注解类元素的默认值
BootstrapMethods类文件JDK7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符
RuntimeVisibleTypeAnnotations类、方法表、字段表、Code属性JDK8中新增的属性,为实现JSR308中新增的类型注解提供的支持,用于指明哪些类注解是运行时(实际上运行时就是进行反射调用)可见的
RuntimeInvisibleTypeAnnotations类、方法表、字段表、Code属性JDK8中新增的属性,为实现JSR308中新增的类型注解提供的支持,与RuntimeVisibleTypeAnnotations属性作用刚好相反,用于指明哪些注解是运行时不可见的
MethodParameters方法表JDK8中新增的属性,用于支持(编译时加上-parameters参数)将方法名称编译进Class文件中,并可运行时获取,此前要获取方法名称(典型的如IDE的代码提示)只能通过JavaDoc中得到
ModuleJDK9中新增的属性,用于记录一个Module的名称以及相关信息(requires、exports、opens、uses、provides)
ModulePackagesJDK9中新增的属性,用于记录一个模块中所有被exports或者opens的包
ModuleMainClassJDK9中新增的属性,用于指定一个模块的主类
NestHostJDK11中新增的属性,用于支持嵌套类(Java中的内部类)的反射和访问控制API,一个内部类通过该属性得知自己的宿主类
NestMembersJDK11中新增的属性,用于支持嵌套类(Java中的内部类)的反射和访问控制API,一个宿主类通过该属性得知自己有哪些内部类

每一个属性的名称都对应常量池中一个CONSTANT_Utf8_info类型的常量,通过常量索引引用,而属性值的结构可以完全自定义的,只需要通过一个u4类型的长度去存储属性值占用的字节数即可,属性表的结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1infoattribute_length

Code

我们定义的方法在经过编译之后,最终会变为字节码指令存储在Code属性内,Code属性出现在方法表的属性集合中,但并非所有的方法表都存在这个属性,例如接口或抽象类中的方法就不存在Code属性。Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件里, Code属性用于描述代码,所有的其他数据项目都用于描述元数据,Code属性结构如下表所示:

类型名称数量含义
u2attribute_name_index1属性名索引值
u4attribute_length1属性值长度
u2max_stack1操作数栈深度的最大值
u2max_locals1局部变量表所需要的存储空间
u4code_length1字节码长度
u1codecode_length字节码指令
u2exception_table_length1异常表长度
exception_infoexception_tableexception_table_length异常表集合
u2attributes_count1属性计数
attribute_infoattributesattributes_count属性集合

attribute_name_index是一项指向常量池中CONSTANT_Utf8_info类型常量的索引值,此常量值固定为Code,它代表了该属性的属性名称。

attribute_length存储了属性值的长度,由于属性名和属性值长度共占用6个字节,因此属性值的长度固定为整个属性表长度减去6个字节。

max_stack存储了操作数栈(Operand Stack)深度的最大值,在方法执行的任意时刻,操作数栈都不会超过这个深度,虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作深度。

max_locals存储了局部变量表所需要的存储空间,max_locals的单位是变量槽(Slot),变量槽是局部变量分配内存时的最小单位。对于bytecharfloatintshortbooleanreturnAddress等长度不超过32位的数据类型,每个局部变量表占用一个变量槽,而doublelong这种64位的数据类型则需要两个变量槽来存放。并不是在方法中用了多少个局部变量,就把这些局部变量所占变量槽数量之和作为max_locals的值,而是会对变量槽进行重用,根据变量的作用域来分配变量槽给各个变量使用,根据同时生存的最大局部变量数量和类型计算出max_locals的大小。

code_length用于存储编译后的字节码长度,虽然它是一个u4类型的长度值,理论上最大值可达到2的32次幂,但是《Java虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令,即实际使用了u2的长度。

code用于存储编译后的字节指令,每一个u1类型的单字节就是一个指令,每当虚拟机读取到code中的一个字节码时,就会去找字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及后续的参数应当如何解析。u1类型的取值范围为0x00~0xFF, 对应十进制的0~255, 也就是一共可以表达256条指令。目前《Java虚拟机规范》已经定义了其中约200条编码值对应的指令含义。

在字节码指令之后的是这个方法的显式异常处理表exception_tableexception_table_length用于记录显示处理异常的个数,也就是catch的异常个数,异常表对于Code属性来说并不是必须存在的。结果如下表所示:

类型名称数量
u2start_pc1
u2end_pc1
u2handler_pc1
u2catch_type1

如果存在异常表,这些字段的含义是如果当字节码从第start_pc行到第end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转到handler_pc处进行处理。

Exceptions

这里的Exceptions属性是在方法表中与Code属性平级的一项属性,不要与前面的讲的异常表混淆,Exceptions属性的作用是列举出方法中可能抛出的受检异常,也就是方法上throws关键字后面列举的异常,它的结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_exceptions1
u2exception_index_tablenumber_of_exceptions

number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常,每一种受查异常使用一个exception_index_table项表示exception_index_table是一个指向常量池中CONSTANT_Class_info类型常量的索引,代表了该受查异常的类型。

LineNumberTable

用于描述Java源码行号与字节码行号(字节码偏移量)之间的对应关系,它并不是运行时必须的属性,但会默认生成到Class文件之中,可以在Javac中使用-g:none-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2line_number_table_length1
line_number_infoline_number_tableline_number_table_length

line_number_table是一个数量为line_number_table_length类型为line_number_info的集合, line_number_info表包含start_pcline_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。

LocalVariableTable&LocalVariableTypeTable

LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中使用-g:none-g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,譬如IDE将会使用诸如arg0arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2local_variable_table_length1
local_variable_infolocal_variable_tablelocal_variable_table_length

其中local_variable_info项代表了一个栈帧与源码中的局部变量的关联,结构如下表所示:

类型名称数量
u2start_pc1
u2length1
u2name_Length1
u2descriptor_length1
u2index1

start_pclength属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。

name_indexdescriptor_index都是指向常量池中CONSTANT_Utf8_info类型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。

index是这个局部变量在栈帧的局部变量表中变量槽的位置。当这个变量数据类型是64位类型时(doublelong),它占用的变量槽为indexindex+1两个。

LocalVariableTypeTableJDK5引入泛型后新增的属性,结构与LocalVariableTable类似,仅仅是把记录的字段描述符的descriptor_index替换成了字段的特征签名Signature。对于非泛型类型来说, 描述符和特征签名能描述的信息是能吻合一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能准确描述泛型类型了。因此出现了LocalVariableTypeTable属性,使用字段的特征签名来完成泛型的描述。

SourceFile&SourceDebugExtension

SourceFile属性用于记录这个Class文件的源码文件名称,这个属性也是可选的,可以使用Javac-g:none-g:source选项来关闭或要求生成这项信息。在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。这个属性是一个定长的属性。结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2sourcefile_index1

sourcefile_index数据项是指向常量池中CONSTANT_Utf8_info类型常量的索引,常量值是源码文件的文件名。

SourceDebugExtension属性是用于存储额外的代码调试信息,为了方便在编译器和动态生成的Class中加入供程序员使用的自定义内容,是JDK5新增的属性。典型的场景是在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号。结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1debug_extension[attribute_length]1

debug_extension存储的就是额外的调试信息,是一组变长UTF-8编码的字符串。一个类中最多只允许存在一个SourceDebugExtension属性。

ConstantValue

用于通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量(类变量)才可以使用这项属性。例如int i1 = 123static int i2 = 123这两种变量虽然类型和值都是一样的,但是虚拟机对这两种变量赋值的方式和时机都有所不同,对于非static修饰的变量(实例变量)的赋值是在实例构造器<init>()方法中进行的。而对于类变量,则有两种方式可以选择,在类构造器<clinit>()方法中或者使用ConstantValue属性。

目前Oracle公司实现的Javac编译器的选择是,如果同时使用finalstatic关键字修饰一个变量(也可称为常量),并且这个变量的数据类型是基本类型或者java.lang.String的话,就将会生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会在<clinit>()方法中进行初始化。

ConstantValue结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2constantvalue_index1

constantvalue_index数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是CONSTANT_Long_infoCONSTANT_Float_infoCONSTANT_Double_infoCONSTANT_Integer_infoCONSTANT_String_info常量中的一种。

InnerClasses

用于记录内部类与宿主类之间的关联关系,如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性,结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_classes1
inner_classes_infoinner_classesnumber_of_classes

number_of_classes代表需要记录多少个内部类信息,每一个内部类的信息都由一个inner_classes_info表进行描述。inner_classes_info表结构如下表所示:

类型名称数量
u2inner_class_info_index1
u2outer_class_info_index1
u2inner_name_index1
u2inner_class_access_flags1

inner_class_info_indexouter_class_info_index都是指向常量池中CONSTANT_Class_info类型常量的索引,分别代表了内部类和宿主类的符号引用。

inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称,如果是匿名内部类,这项值为0。

inner_class_access_flags是内部类的访问标志,类似于类的access_flags, 它的取值范围如下表所示:

标志名称标志值含义
ACC_PUBLIC0x0001是否为public
ACC_PRIVATE0x0002是否为private
ACC_PROTECTED0x0004是否为protected
ACC_STATIC0x0008是否为static
ACC_FINAL0x0010是否为final
ACC_INTERFACE0x0020是否为接口
ACC_ABSTRACT0x0400是否为abstract
ACC_SYNTHETIC0x1000标识内部类并非由用户代码产生的
ACC_ANNOTATION0x2000是不是一个注解
ACC_ENUM0x4000是不是一个枚举

Deprecated&Synthetic

DeprecatedSynthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。

Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过代码中使用“@Deprecated”注解进行设置

Synthetic属性用于表示字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的,在JDK5之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们访问标志中的ACC_SYNTHETIC标志位。所有由不属于用户代码产生的类、方法及字段都应当至少设置Synthetic属性或者ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器“<init>()”方法和类构造器“<clinit>()”方法。

DeprecatedSynthetic结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1

attribute_length数据项的值必须为0x00000000, 因为没有任何属性值需要设置。

StackMapTable

此属性是在JDK6增加到Class文件规范中的,它是一个相当复杂的变长属性,位于Code属性的属性表中。这个属性会在类加载的字节码验证阶段被类型检查验证器所使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推到验证器。

此属性中包含零至多个栈映射帧(Stack Map Frame),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。其结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_entries1
stack_map_framestack_map_frame_entriesnumber_of_entries

JavaSE 7版之后的《Java虚拟机规范》中,明确规定对于版本号大于或等于50.0的Class文件,如果方法的Code属性中没有附带StackMapTable属性,那就意味着它带有一个隐式的StackMap属性,这个StackMap属性的作用等同于number_of_entries值为0的StackMapTable属性。一个方法的Code属性最多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常。

Signature

此属性是在JDK5增加到Class文件规范中的,是一个可选的定长属性,可以出现在类、字段表和方法表结构的属性表中。在任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variable)或参数化类型(Parameterized Type),则Signature属性会为它记录泛型签名信息。

之所以要使用一个属性专门去记录泛型签名信息,是因为Java是用擦除法实现的伪泛型,字节码中所有的泛型信息编译(类型变量、参数化类型)在编译之后都通通被擦除掉,导致运行时无法获取泛型类型,Signature属性就是为了弥补这个缺陷而增加的,Java的反射能够获取的泛型类型,最终的数据来源也是这个属性。

Signature结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2signature_index1

signature_index的值必须是一个对常量池的有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示类签名或方法类型签名或字段类型签名。如果当前的Signature属性是类文件的属性,则这个结构表示类签名,如果当前的Signature属性是方法表的属性,则这个结构表示方法类型签名,如果当前Signature属性是字段表的属性,则这个结构表示字段类型签名。

BootstrapMethods

此属性是在JDK7增加到Class文件规范中的,它是一个复杂的变长属性,位于类文件的属性表中,用于保存InovkeDynamic指令引用的引导方法限定符。

根据《Java虚拟机规范》(从Java SE 7版起)的规定,如果某个类文件结构的常量池中曾经出现过CONSTANT_InvokeDynamic_info类型的常量,那么这个类文件的属性表中必须存在一个明确的BootstrapMethods属性,另外,即使CONSTANT_InvokeDynamic_info类型的常量在常量池中出现过多次,类文件的属性表中最多也只能有一个BootstrapMethods属性。

虽然JDK7中已经提供了InovkeDynamic指令,但这个版本的Javac编译器还暂时无法支持InvokeDynamic指令和生成BootstrapMethods属性,必须通过一些非常规的手段才能使用它们。直到JDK8Lambda表达式和接口默认方法的出现,InvokeDynamic指令才算在Java语言生成的Class文件中有了用武之地。

BootstrapMethods结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2num_bootstrap_methods1
bootstrap_methodbootstrap_methodsnum_bootstrap_methods

其中bootstrap_method结构如下表所示:

类型名称数量
u2bootstrap_method_ref1
u2num_bootstrap_arguments1
u2bootstrap_argumentsnum_bootstrap_arguments

num_bootstrap_methods存储了bootstrap_methods的个数,而bootstrap_methods[]数组的每个成员包含了一个指向常量池CONSTANT_MethodHandle结构的索引值,它代表了一个引导方法。还包含了这个引导方法静态参数的序列(可能为空)。

bootstrap_methods[]数组的每个成员必须包含以下三项内容:

  • bootstrap_method_ref:此项的值必须是一个对常量池的有效索引,常量池在该索引处的值必须是一个CONSTANT_MethodHandle_info结构
  • num_bootstrap_arguments:此项的值给出了bootstrap_argu-ments[]数组成员的数量
  • bootstrap_arguments:此数组的每个成员必须是一个对常量池的有效索引,常量池在该索引出必须是下列结构之一:CONSTANT_String_infoCONSTANT_Class_infoCONSTANT_Integer_infoCONSTANT_Long_infoCONSTANT_Float_infoCONSTANT_Double_infoCONSTANT_MethodHandle_infoCONSTANT_MethodType_info

MethodParameters

此属性是在JDK8增加到Class文件格式中的,它是一个用在方法表中的变长属性,用来记录方法的各个形参名称和信息。

在前面我们讲到可以通过-g: vars参数将方法参数的名称生成到LocalVariableTable属性之中,LocalVariableTable属性是Code属性的子属性,有方法体的时候可以这样做,但是像抽象方法和接口方法这类方法本身就没有方法体,也就不会有LocalVariableTable属性,此时就没有一个完整保留方法参数签名的地方了。所以JDK8新增了这个属性,在编译时可以加上-parameters参数,将方法名称也写进Class文件中。结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1parameters_count1
parameterparametersparameters_count

其中parameter结构如下表所示:

类型名称数量
u2name_index1
u2access_flags1

name_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,代表了该参数的名称。

access_flags是参数的状态指示器, 它可以包含以下三种状态中的一种或多种:

  • ACC_FINAL(0x0010):表示该参数被final修饰
  • ACC_SYNTHETIC(0x1000):表示该参数并未出现在源文件中,是编译器自动生成的
  • ACC_MANDATED(0x8000):表示该参数是在源文件中隐式定义的,典型的场景就是this关键字

模块化相关属性

模块化是JDK9新增的一个重量级功能,因为模块描述文件(module-info.java)最终是要编译成一个独立的Class文件来存储的,所以,Class文件格式也扩展了ModuleModulePackagesModuleMainClass三个属性用于支持Java模块化相关功能。

Module属性是一个非常复杂的变长属性,除了表示该模块的名称、版本、标志信息以外,还存储了这个模块requiresexportsopensusesprovides定义的全部内容。结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2module_name_index1
u2module_flags1
u2module_version_index1
u2requires_count1
requirerequiresrequires_count
u2exports_count1
exportexportsexports_count
u2opens_count1
openopensopens_count
u2uses_count1
useuses_indexuses_count
u2provides_count1
provideprovidesprovides_count

module_name_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,代表了该模块的名称。

module_flags是模块的状态指示器,它可以包含以下三种状态中的一种或多种:

  • ACC_OPEN(0x0020):表示该模块是开放的
  • ACC_SYNTHETIC(0x1000):表示该模块并未出现在源文件中,是编译器自动生成的
  • ACC_MANDATED(0x8000):表示该模块是在源文件中隐式定义的

requiresexportsopensusesprovides几个属性的结构都类似,此处就以exports为例进行讲解。exports属性的每一元素都代表一个被模块所导出的包,其结构如下表所示:

类型名称数量
u2exports_index1
u2exports_flags1
u2exports_to_count1
exportexport_to_indexexports_to_count

exports_index是一个指向常量池CONSTANT_Package_info常量的索引值,代表了被该模块导出的包。

exports_flags是该导出包的状态指示器,它可以包含以下两种状态中的一种或多种:

  • ACC_SYNTHETIC(0x1000):表示该导出包并未出现在源文件中,是编译器自动生成的
  • ACC_MANDATED(0x8000):表示该导出包是在源文件中隐式定义的

exports_to_count是该导出包的限定计数器,如果这个计数器为零,这说明该导出包是无限定的(Unqualified),即完全开放的,任何其他模块都可以访问该包中所有内容。如果该计数器不为零,则后面的exports_to_index是以计数器值为长度的数组,每个数组元素都是一个指向常量池中CONSTANT_Module_info常量的索引值,代表着只有在这个数组范围内的模块才被允许访问该导出包的内容。

ModulePackages是另一个用于支持Java模块化的变长属性,它用于描述该模块中所有的包,不论是不是被export或者open的。其结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2package_count1
u2package_indexpackage_count

package_countpackage_index数组的计数器,package_index中每个元素都是指向常量池CONSTANT_Package_info常量的索引值,代表了当前模块中的一个包。

ModuleMainClass属性是一个定长属性,用于确定该模块的主类(Main Class)。其结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2main_class_index1

main_class_index是一个指向常量池CONSTANT_Class_info常量的索引值,代表了该模块的主类。

运行时注解相关属性

JDK5时期,Java提供了注解支持(Annotation),为了存储源码中的注解信息,将RuntimeVisibleAnnotationsRuntimeInvisibleAnnotationsRuntimeVisibleParameterAnnotationsRuntimeInvisibleParameterAnnotations四个属性增加到了Class文件格式中。到了JDK8时期,进一步加强了注解使用范围,又新增了类型注解,所以又增加了RuntimeVisibleTypeAnnotationsRuntimeInvisibleTypeAnnotations两个属性,这6个属性不论结构还是功能都比较雷同,此处以RuntimeVisibleAnnotations为例进行讲解。

RuntimeVisibleAnnotations是一个变长属性,它记录了类、字段或方法的声明上记录运行时可见注解,当我们使用反射来获取类、字段或方法上的注解时,返回值就是通过这个属性来取到的。结构如下表所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2num_annotations1
annotationannotationsnum_annotations

num_annotations存储的annotations数组的元素个数,annotations中每个元素都代表了一个运行时可见的注解,注解在Class文件中以annotation结构来存储。annotation结构如下表所示:

类型名称数量
u2type_index1
u2num_element_value_pairs1
element_value_pairelement_value_pairsnum_element_value_pairs

type_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,该常量应以字段描述符的形式表示一个注解。num_element_value_pairs存储的element_value_pairs数组的元素个数, element_value_pairs中每个元素都是一个键值对,代表该注解的参数和值