CLASS类文件格式

CLASS类文件格式

Gat1ta 1,096 2022-06-20

学习一下Class文件格式,本篇文章主要记录一下格式细节,并在文章结尾通过代码简单解析一个Class文件。

CLASS文件格式

根据《JAVA虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:“无符号数”和“表”。

  • 无符号数属于基本的数据类型,以u1/u2/u4/u8来分别代表1个字节2个字节4个字节和8个字节的无符号数,无符号数可以用来描述文字/索引引用/数量值或者按照UTF-8编码构成的字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的符合结构数据,整个Class文件本质上也可以视为一张表。

无论是无符号数还是表,当需要描述同一类型数量但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型数据为某一类型的集合。

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

魔数

每个Class文件的头4字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。

文件版本

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6字节是次版本号(Minor Version),第7和第8字节存储的是主版本号(Major Version)。 JAVA的版本号是从45开始的,JDK1.1之后每个JDK大版本发布主版本号向上+1。
高版本的JDK可以向下兼容低版本的Class文件,但是不能运行以后版本的Class文件,因为《JAVA虚拟机规范》在Class文件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

常量池

紧接着住次版本号之后就是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一。
由于常量池中常量的数据是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(Constant_pool_count)。与JAVA中语言习惯不同,这个容量计数是从1而不是从0开始的。这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。

常量池中每一个常量都是一个表,最初常量表中共有11种结构各不相同的表结构数据,后来为了更好的支持动态语言调用,额外增加了4种动态语言相关常量,为了支持JAVA模块化系统(Jigsaw),又加入了CONSTANT_MODULE_INFO和CONSTANT_PACKAGE_INFO两个常量,所以截至JDK13,常量表中分别有17种不同类型的常量。
如果有过学习PE文件格式的经验,会发现这个常量表和PE格式的数据目录有点相似。

常量池的项目类型:

类型 标志 描述
CONSTANT_UTF8_INFO 1 UTF-8编码的字符串
CONSTANT_INTEGER_INFO 3 整形字面量
CONSTANT_FLOAT_INFO 4 浮点型字面量
CONSTANT_LONG_INFO 5 长整型字面量
CONSTANT_DOUBLE_INFO 6 双精度浮点型字面量
CONSTANT_CLASS_INFO 7 类或接口的符号引用
CONSTANT_STRING_INFO 8 字符串类型字面量
CONSTANT_FIELDREF_INFO 9 字段的符号引用
CONSTANT_METHODREF_INFO 10 类中方法的符号引用
CONSTANT_INTERFACEMETHODREF_INFO 11 接口中方法的符号引用
CONSTANT_NAMEANDTYPE_INFO 12 字段或方法的部分符号引用
CONSTANT_METHODHANDLE_INFO 15 表示方法句柄
CONSTANT_METHODTYPE_INFO 16 表示方法类型
CONSTANT_DYNAMIC_INFO 17 表示一个动态计算常量
CONSTANT_INVOKEDYNAMIC_INFO 18 表示一个动态方法调用点
CONSTANT_MODULE_INFO 19 表示一个模块
CONSTANT_PACKAGE_INFO 20 表示一个模块中开放或者导出的包

之所以说常量池是最繁琐的数据,是因为这17种常量类型各自有着完全独立的数据结构,两两之间并没有什么共性和联系,因此只能逐项进行讲解。

CONSTANT_UTF8_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为1
u2 length 1 UTF-8编码的字符串占用的字节数
u1 bytes length 长度为length的UTF-8编码的字符串

length值说明了这个UTF-8编码的字符串长度是多少字节,它后面紧跟着的长度为length字节的连续数据是一个使用了UTF-8略缩编码表示的字符串。
UTF-8略缩编码与普通UTF-8编码的区别是:从’\u0001’到’u007f’之间的字符(相当于1-127的ASCII码)的缩略编码使用一个字节表示,从’\u0080’到’\u07ff’之间的所有字符的缩略编码用两个字节表示,从’\u0800’开始到’\uffff’之间的所有字符的缩略编码就按照普通的UTF-8编码规则使用三个字节表示。

CONSTANT_INTEGER_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为3
u4 bytes 1 按照高位在前存储的int值

CONSTANT_FLOAT_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为4
u4 bytes 1 按照高位在前存储的float值

CONSTANT_LONG_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为5
u8 bytes 1 按照高位在前存储的long值

CONSTANT_DOUBLE_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为6
u8 bytes 1 按照高位在前存储的double值

CONSTANT_CLASS_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为7
u2 name_index 1 指向全限定名常量项的索引

tag是标志位,它用于区分常量类型;name_index是常量池的索引值,它指向常量池中一个CONSTANT_UTF8_INFO类型常量,此常量代表这个类(或者接口)的全限定名。

CONSTANT_STRING_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为8
u2 bytes 1 指向字符串字面量的索引

CONSTANT_FIELDREF_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为9
u2 index 1 指向声明字段的类或者接口描述符CONSTANT_CLASS_INFO的索引项
u2 index 1 指向字段描述符CONSTANT_NAMEANDTYPE_INFO的索引项

CONSTANT_METHODREF_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为10
u2 index 1 指向声明方法的类描述符CONSTANT_CLASS_INFO的索引项
u2 index 1 指向名称及类型描述符CONSTANT_NAMEANDTYPE_INFO的索引项

CONSTANT_INTERFACEMETHODREF_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为11
u2 index 1 指向声明方法的接口描述符CONSTANT_CLASS_INFO的索引项
u2 index 1 指向名称及类型描述符CONSTANT_NAMEANDTYPE_INFO的索引项

CONSTANT_NAMEANDTYPE_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为12
u2 index 1 指向该字段或方法名称常量项的索引
u2 index 1 指向该字段或方法描述符常量项的索引

CONSTANT_METHODHANDLE_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为15
u1 reference_kind 1 值必须在1至9之间(包括1和9),它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
u2 reference_index 1 值必须是对常量池的有效索引

CONSTANT_METHODTYPE_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为16
u2 descriptor_index 1 值必须是对常量池有效的索引,常量池在该索引处的项必须是CONSTANT_UTF8_INFO结构

CONSTANT_DYNAMIC_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为17
u2 bootstrap_method_attr_index 1 值必须是对当前Class文件中引导方法表的bootstrap_method[]数组的有效索引
u2 name_and_type_index 1 值必须是对当前常量池的有效索引,常量池在该处的项必须是CONSTANT_NAMEANDTYPE_INFO结构,表示方法名和方法描述符

CONSTANT_INVOKEDYNAMIC_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为18
u2 bootstrap_method_attr_index 1 值必须是对当前Class文件中引导方法表的bootstrap_method[]数组的有效索引
u2 name_and_type_index 1 值必须是对当前常量池的有效索引。常量池在该索引处的项必须是CONSTANT_NAMEANDTYPE_INFO结构,表示方法名和方法描述符

CONSTANT_MODULE_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为19
u2 name_index 1 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_UTF8_INFO结构,表示模块名

CONSTANT_PACKAGE_INFO型常量结构

类型 名称 数量 描述
u1 tag 1 值为20
u2 name_index 1 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_UTF8_INFO结构,表示包名称
u2 index 1 指向该字段或方法描述符常量项的索引

访问标志

在常量池结束后,紧接着的两个字节代表访问标志(Acessflag),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为Public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。详细标志位及标志含义见下表:

标志名称 标志值 含义
ACC_PUBLIC 0X0001 是否为public类型
ACC_FINAL 0X0010 是否被声明为final,只有类可以设置
ACC_SUPER 0X0020 是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义在JDK1.0.2发生过改变,为了区别这条指令使用哪种语义,JDK1.0.2之后编译出来的类这个标志必须为真。
ACC_INTERFACE 0X0200 标识这是一个接口
ACC_ABSTRACT 0X0400 是否为abstract类型,对于接口或者抽象类来说,此标志为真,其他类型为假。
ACC_SYNTHETIC 0X1000 标识这个类并非由用户代码产生的
ACC_ANNOTATION 0X2000 标识这是一个注解
ACC_ENUM 0X4000 标识这是一个枚举
ACC_MODULE 0X8000 标识这是一个模块

Accessflag一共有16个标志位可以使用,当前只定义了其中9个,没有使用到的标志位要求一律为零。

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

类索引(this class)和父类索引(super class)都是一个U2类型的数据,而接口索引集合(interfaces)是一组U2类型的集合,Class文件中这三项数据来确定该类型的继承关系。
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于JAVA类都有父类,因此除了java.lang.object外,所有java类的父类索引都不为0.
接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个class文件标识的是一个接口,则应当是extends关键字)后的接口顺序从左到右排列在接口索引集合中。
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个U2类型的索引值标识,它们各自指向一个类型为CONSTANT_CLASS_INFO的类描述符常量,通过CONSTANT_CLASS_INFO类型的常量中的索引值可以找到定义在CONSTANT_UTF8_INFO类型常量中的全限定名字符串。
对于接口索引集合,入口的第一项U2类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不在占用任何字节。

字段表集合

接下来就是字段表集合了,字段表(field_info)用于描述接口或者类中声明的变量。JAVA语言中的“字段”包括类级变量以及实例型变量,但不包括在方法内部声明的局部变量。
紧跟接口索引集合后,第一个U2类型的数据是字段表计数器,表示字段表的数量。下图为字段表结构:

类型 名称 数量
U2 access_flag 1
U2 name_index 1
U2 descriptor_Index 1
U2 attributes_count 1
attribute_info attributes attributes_count

字段修饰符放在access_flag项中,它与类中的access_flags项是非常相似的,都是一个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类型

跟随access_flag标志的是两项索引值:name_Index和descriptor_index。它们都是对常量池项的引用,分别代表着字段的简单名称以及字段和方法的描述符。
现在需要解释一下“简单名称”“描述符”以及前面多次出现的“全限定名”这三种字符串的概念。
全限定名仅仅是把类全名中的".“替换成了”/“而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个”;“号表示全限定名结束。
简单名称则就是指没有类型和参数修饰符的方法或字段名称。比如inc()方法和m字段的简单名称分别就是"inc"和"m”。
相比全限定名和简单名称,方法和字段的描述符就复杂一些。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,详见下图:

标识字符 含义
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;”,一个整型数组“int[]”将被记录成“[I”。
用描述符来描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号”()“内。如方法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"
字段表所包含的固定数据项目到descriptor_index为止就全部结束了,不过在descriptor_index之后跟随着一个属性表集合,用于存储一些额外的信息,字段表可以在属性表中附加描述零至多项的额外信息。属性表相关信息将在介绍属性表集合时再做进一步讲解。
字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本java代码中不存在的字段,比如在内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字段。

方法表集合

Class文件存储格式对方法的描述与对字段的描述采用了几乎完全一致的方式,放发表的结构如同字段表一样,一次包括访问标志、名称索引、描述符索引、属性表集合几项。
如下图所示,这些数据项目含义也与字段表中非常类似,仅在访问标志和属性表集合的可选项有所区别。

类型 名称 数量
U2 access_flag 1
U2 name_index 1
U2 descriptor_Index 1
U2 attributes_count 1
attribute_info attributes attributes_count

对于方法表,所有标志位及其取值可见下表:

标志名称 标志值 含义
ACC_PUBLIC 0X0001 字段是否为public类型
ACC_PRIVATE 0X0002 字段是否为private类型
ACC_PROTECTED 0X0004 字段是否为protected类型
ACC_STATIC 0X0008 字段是否为static类型
ACC_FINAL 0X0010 字段是否为final类型
ACC_SYNCHRONIZED 0X0020 字段是否为synchronized类型
ACC_BRIDGE 0X0040 字段是不是由编译器产生的桥接方法
ACC_VARARGS 0X0080 字段是否接受不定参数
ACC_NATIV 0X0100 字段是否为native类型
ACC_ABSTRACT 0X0400 字段是否为abstract类型
ACC_STRICT 0X0800 字段是否为strictfp类型
ACC_SYNTHETIC 0X1000 字段是否由编译器自动产生

属性表集合

与Class文件中其他数据项目要求严格的顺序、长度和内容不同,属性表集合的稍微宽松一些,不再要求各个属性表具有严格的顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。
在最新的《Java虚拟机规范》的JavaSe12版本中,预定义属性已经增加到29项,具体见下表:

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 由final关键字定义的常量值
Deprecated 类、方法表、字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常列表
EnclosingMethod 类文件 仅当一个类为局部类或者匿名类时才会拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
StakcMapTable Code属性 JDK6新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature 类、方法表、字段表 JDK5新增的属性,用于支持泛型情况下的方法签名。在JAVA语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于JAVA的泛型擦除法实现,为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息。
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 JDK5中新增的属性,用于存储额外的调试信息。比如在进行JSP文件调试时,无法通过jAVA堆栈来定位到JSP文件的行号,JSR45 提案为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用该属性就可以用于存储这个标准所新加入的调试信息
Synthetic 类、方法表、字段表 标识方法或字段为编译器自动生成的
LocalVariableTypeTable JDK5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
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文件中,并可运行时获取。此前要获取方法名称只能通过JavaDoc得到。
Module JDK9中新增的属性,用于记录一个Module的名称以及相关信息(requires、exports、opens、uses、provides)
ModulePackages JDK9中新增的属性,用于记录一个模板中所有被exports或者open的包
ModuleMainClass JDK9中新增的属性,用于执行一个模块的主类
NestHost JDK11中新增的属性,用于支持嵌套类(Java中的内部类)的反射和访问控制的API,一个内部类通过该属性得知自己的宿主类
NestMembers JDK11新增的属性,用于支持嵌套类(Java中的内部类)的反射和访问控制的API,一个宿主类通过该属性得知自己有哪些内部类。

对于每一个属性,它的名称都要从常量池中引用一个CONSTANT_UTF8_INFO类型的常量来标识,而属性值的结构则是完全自定义的,只需要通过U4的长度属性去说明属性值所占用的位数即可。
一个符合规则的属性表应该满足下表结构:

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U1 info attribute_length

Code属性

JAVA程序方法体里面的代码经过javac编译器处理后,最终会变为字节码存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,比如接口或者抽象类的方法就不存在Code属性,如果方法表有Code属性存在,那么它的结构如下所示:

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 max_stack 1
U2 max_locals 1
U4 code_length 1
U1 code code_length
U2 exception_table_length 1
exception_info exception_table exception_table_length
U2 attributes_count 1
attribute_info attributes attributes_count

attribute_name_index是一项指向CONSTANT_UTF8_INFO型常量的索引,此常量值固定为“Code”,它代表了该属性的属性名称,attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6个字节,索引属性值的长度固定为整个属性表长度减去6个字节。
max_stack代表了操作数栈深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行时需要根据这个值来分配栈帧中的操作栈深度。
max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是变量槽,变量槽是虚拟机为变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这种64位的数据类型则需要两个变量槽来存放。
code_length和code用来存储java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。既然叫做字节码指令,顾名思义每个指令就是一个U1类型的单字节,当虚拟机读取到Code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及后续的参数应当如何解析。
关于code_length,有一件值得注意的事,虽然它是一个U4类型的长度值,理论上最大值可以达到2的32次幂,但是《JAVA虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令,即实际只是用了U2的长度,如果超过了这个长度,javac编译器会拒绝编译。
在字节码指令之后的是这个方法的显式异常处理表集合,异常表对于Code属性来说不是必须存在的。
如果存在异常表,那它的格式如下表所示,包含四个字段,这些字段的含义为:
如果当字节码从第start_pc行到第end_pc行之间(不含end_pc行)出现了类型为catch_type或者其他子类的异常(catch_type为指向一个CONSTANT_CLASS_INFO型常量的索引),则转到第handler_pc行继续处理。当catch_type为零时,代表任何异常情况都需要转到handler_pc处进行处理。

类型 名称 数量
U2 start_pc 1
U2 end_pc 1
U2 handler_pc 1
U2 catch_pc 1

Exception属性

这里的Exceptions属性是在方法表中与Code属性平级的一项属性,读者不要与前面刚刚讲解完的异常表产生混淆。Exception属性的作用是列举出方法中可能抛出的受查异常(Checked Exception),也就是方法描述时在throws关键字后面列出的异常,它的结构如下表:

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 number_of_exceptions 1
U2 exception_index_table number_of_exceptions

此属性中的number_of_exceptions项表示方法可能抛出的number_of_exceptions种受查异常,每一种受查异常使用一个exception_index_table项表示;exception_index_table事一个指向常量池中CONSTANT_CLASS_INFO型常量的索引,代表该受查异常的类型。

LineNumberTable属性

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

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 line_number_table_length 1
line_number_info line_number_table line_number_table_length

line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包含start_pc和line_number两个U2类型的数据项,前者是字节码行号,后者是源代码行号。

LocalVariableTable及LocalVaribleTypeTable属性

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

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 local_variable_table_length 1
local_variable_info local_variable_table local_variable_table_length

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

类型 名称 数量
U2 start_pc 1
U2 length 1
U2 name_index 1
U2 descriptor_index 1
U2 index 1

start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。
name_index和descriptor_index都是指向常量池中CONSTANT_UTF8_INFO型的常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。
index是这个局部变量在栈帧的局部变量表中变量槽的位置。当这个变量数据类型是64位类型时,它占用的变量槽为index和index+1两个。
在JDK5引入泛型后,LocalVariableTable属性增加了一个“姐妹属性”-LocalVariableTypeTable。这个新增的属性结构与LocalVariableTable非常相似,仅仅是把记录的字段描述符的descriptor_index替换成了字段的特征签名(Signature)。对于非泛型的类型来说,描述符和特征签名能描述的信息是能吻合一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能准确描述泛型类型了。因此出现LocalVariableTypeTable属性,使用字段的特征签名来完成泛型的描述。

SourceFile及SourceDebugExtension属性

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

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 sourcefile_index 1

sourcefile_index数据项是指向常量池中CONSTANT_UTF8_INFO常量项的索引,常量值就是源码文件的文件名。
为了方便在编译器和动态生成的Class中加入供程序员使用的自定义内容,在JDK5时,新增了SourceDebugExtension属性用于存储额外的代码调试信息。结构如下所示:

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U1 debug_extension[attribute_length] 1

其中debug_extension存储的就是额外的调试信息,是一组通过变长UTF-8格式来表示的字符串。一个类中最多只允许存在一个SourceDebugExtension属性。

ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。
对于非static类型的变量(实例变量)的赋值是在实例构造器()方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器()方法中或者使用ConstantValue属性。目前oracle公司实现的javac编译器的选择是,如果同时使用final和static来修饰一个变量(按照习惯,这里称常量更贴切),并且这个变量的数据类型是基本类型或者String的话,就会生成ConstantValue属性来进行初始化;如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在()方法中初始化。
ConstantValue属性结构如下:

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 constantvalue_index 1

从结构上可以看出ConstantValue属性是一个定长属性,它的attribute_length数据项值必须固定为2。constantvalue_index数据项代表一个字面量常量的引用,根据字段类型不同,字面量可以是CONSTANT_LONG_INFO、CONSTANT_FLOAT_INFO、CONSTANT_DOUBLE_INFO、CONSTANT_INTEGER_INFO和CONSTANT_STRING_INFO常量中的一种。

InnerClasses属性

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

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 number_of_classes 1
inner_classes_info inner_classes number_of_classes

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

类型 名称 数量
U2 inner_class_info_index 1
U2 outer_class_info_index 1
U2 inner_name_index 1
U2 inner_name_access_flags 1

inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_CLASS_INFO型常量的索引,分别代表了内部类和宿主类的符号引用。
inner_name_index是指向常量池中CONSTANT_UTF8_INFO型常量的索引,代表这个内部类的名称,如果是匿名内部类,这项值为0。
inner_class_access_flags是内部类的访问标志,类似于类的access_flags,它的取值范围如下图:

标志名称 标志值 含义
ACC_PUBLIC 0X0001 内部类是否为public
ACC_PRIVATE 0X0002 内部类是否为private
ACC_PROTECTED 0X0004 内部类是否为protected
ACC_STATIC 0X0008 内部类是否为static
ACC_FINAL 0X0010 内部类是否为final
ACC_INTERFACE 0X0200 内部类是否为接口
ACC_ABSTRACT 0X0400 内部类是否为abstract
ACC_SYNTHETIC 0X1000 内部类是否并非由用户代码产生
ACC_ANNOTATION 0X2000 内部类是否为注解
ACC_ENUM 0X4000 内部类是否为枚举

Deprecated及Synthetic属性

Deprecated和Synthetic属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。
Deprecated属性用于表示某个类、字段或者方法已经被程序作者定位不再推荐使用,它可以通过代码中使用“@deprecated”注解进行设置。
Synthetic属性代表此字段或者方法并不是由java源码直接产生的,而是由编译器自行添加的,在JDK5之后,标识一个类、字段或者方法是编译器自动产生的也可以设置它们访问标志中的ACC_SYNTHETIC标志位。
Deprecated和Synthetic属性的结构非常简单,如下所示:

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1

StackMapTable属性

StackMapTable属性在JDK6增加到Class文件规范之中,它是一个相当复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(type checker)使用,目的在于代替以前比较小号性能的基于数据流分析的类型推导验证器。
StackMapTable属性中包含零至多个栈映射帧(stack map frame),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。StackMapTable属性的结构如下所示:

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 number_of_entries 1
stack_map_frame stack_map_frame_entries number_of_entries

Signature属性

Signature属性在JDK5增加到Class文件规范之中,它是一个可选的定长属性,可以出现于类、字段表和方法表结构的属性表中。在JDK5里面大幅增强了java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则Signature属性会为它记录泛型签名信息。
Signature属性如下图:

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 signature_index 1

其中signature_index项的值必须是一个对常量池的有效索引。常量池在该索引处的项必须是CONSTANT_UTF8_INFO结构,表示类签名或方法类型签名或字段类型签名。

BootstrapMethods属性

BootstrapMethods属性在JDK7时增加到Class文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。
根据《java虚拟机规范》(JAVA SE7版起)的规定,如果某个类文件结构的常量池中曾经出现过CONSTANT_INVOKEDYNAMIC_INFO类型的常量,那么这个类文件的属性表必须存在一个明确的BootstrapMethods属性,另外,即使CONSTANT_INVOKEDYNAMIC_INFO类型的常量在常量池中出现过多次,类文件的属性表中最多只能有一个BootstrapMethods属性。
BootstrapMethods属性的结构如下图:

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 num_bootstrap_methods 1
bootstrap_method bootstrap_methods num_bootstrap_methods

其中引用到的bootstrap_method结构下图:

类型 名称 数量
U2 bootstrap_method_ref 1
U2 num_bootstrap_arguments 1
U2 bootstrap_arguments 1

显而易见,BootstrapMethods属性中num_bootstrap_methods字段是bootstrap_methods中的bootstrap_method元素个数。
而bootstrap_methods数组的每个成员必须包含一下三项内容:

  1. bootstrap_method_ref:该项的值必须是一个对常量池的有效索引。常量池在该索引处的值必须是一个CONSTANT_METHODHANDLE_INFO结构。
  2. num_bootstrap_arguments:该项的值给出了bootstrap_arguments数组成员的数量。
  3. bootstrap_arguments:该项数组的每个成员必须是一个对常量池的有效索引。常量池在该索引处必须是下列结构之一:CONSTANT_STRING_INFO、CONSTANT_CLASS_INFO、CONSTANT_INTEGER_INFO、CONSTANT_LONG_INFO、CONSTANT_FLOAT_INFO、CONSTANT_DOUBLE_INFO、CONSTANT_METHODHANDLE_INFO或CONSTANT_METHODTYPE_INFO。

MethodParameters属性

MethodParameters是在JDK8时新加入到Class文件格式的,它是一个用在方法表中的变长属性。MethodParameters的作用是记录方法的各个形参名称和信息。
MethodParameters属性的结构如下图:

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U1 parameters_count 1
parameter parameters parameters_count

其中,引用的parameter结构如下所示:

类型 名称 数量
U2 name_index 1
U2 access_flags 1

其中,name_index是一个指向常量池CONSTANT_UTF8_INFO常量的索引值,代表了该参数的名称。而access_flags是参数的状态指示器,它可以包含以下三种状态中的一种或多种:

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

模块化相关属性

JDK9的一个重量级功能是Java的模块化功能,因为模块描述文件最终要编译成一个独立的Class文件来存储的,所以,Class文件格式也扩展了Module、ModulePackages和ModuleMainClass三个属性用于支持Java模块化相关功能。
Module属性是一个非常复杂的变长属性,除了表示该模块的名称、版本、标志信息以外,还存储了这个模块requires、exports、opens、uses和provides定义的全部内容,其结构如下所示:

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 module_name_index 1
U2 module_flags 1
U2 module_version_index 1
U2 requires_count 1
require requires requires_count
U2 exports_count 1
export exports exports_count
U2 opens_count 1
open opens opens_count
U2 uses_count 1
use uses uses_count
U2 provides_count 1
provide provides provides_count

其中,module_name_index是一个指向常量池CONSTANT_UTF8_INFO常量的索引值,代表了该模块的名称。而module_flags是模块的状态指示器,它可以包含以下三种状态的一种或多种:

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

module_version_index是一个指向常量池CONSTANT_UTF8_INFO常量的索引值,代表了该模块的版本号。
后续的几个属性分别记录了模块的requires、exports、opens、uses和provides定义,由于它们的结构是基本相似的,所以这里仅介绍其中的exports,该属性结构如下:

类型 名称 数量
U2 exports_index 1
U2 exports_flags 1
U2 exports_to_count 1
export exports_to_index exports_to_count

exports属性的每一个元素都代表了一个被模块所导出的包,exports_index是一个指向常量池CONSTANT_PACKAGE_INFO常量的索引值,代表了被该模块导出的包。exports_flags是该导出包的状态指示器,它可以包含以下两种状态中的一种或多种:

  • 0x1000(ACC_SYNTHETIC):表示该导出包并未出现在源文件中,是编译器自动生成的。
  • 0x8000(ACC_MANDATED):表示该导出包是在源文件中隐式定义的。
    exports_to_count是该导出包的限定计数器,如果这个计数器为零,这说明该导出包是无限定的,即完全开放的,任何其他模块都可以访问该包中的所有内容。如果该计数器不为零,则后面的export_to_index是以计数器值为长度的数组,每个数组元素都是一个指向常量池CONSTANT_MODULE_INFO的常量值,代表着只有在这个数组范围内的模块才被允许访问该导出包的内容。

ModulePackages属性

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

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 package_count 1
U2 package_index package_count

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

ModuleMainClass属性

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

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 main_class_index 1

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

RuntimeVisibleAnnotations属性

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

类型 名称 数量
U2 attribute_name_index 1
U4 attribute_length 1
U2 num_annotations 1
annotation annotations num_annotations

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

类型 名称 数量
U2 type_index 1
U2 num_element_value_pairs 1
element_value_pair element_value_pairs num_element_value_pairs

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

代码实现

接下来用代码简单解析一个类文件,为了方便复制,我把所有代码写在了一个文件中。代码写的比较烂,将就看把~

package LanguageFeatures.ClassInfo;
import com.sun.xml.internal.messaging.saaj.util.ByteInputStream;
import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream;
import org.omg.CORBA.Current;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;




class ConstantType{
  private final static int CONSTANT_UTF8_INFO=1;
  private final static int CONSTANT_INTEGER_INFO=3;
  private final static int CONSTANT_FLOAT_INFO=4;
  private final static int CONSTANT_LONG_INFO=5;
  private final static int CONSTANT_DOUBLE_INFO=6;
  private final static int CONSTANT_CLASS_INFO=7;
  private final static int CONSTANT_STRING_INFO=8;
  private final static int CONSTANT_FIELDREF_INFO=9;
  private final static int CONSTANT_METHODREF_INFO=10;
  private final static int CONSTANT_INTERFACEMETHODREF_INFO=11;
  private final static int CONSTANT_NAMEANDTYPE_INFO=12;
  private final static int CONSTANT_METHODHANDLE_INFO=15;
  private final static int CONSTANT_METHODTYPE_INFO=16;
  private final static int CONSTANT_DYNAMIC_INFO=17;
  private final static int CONSTANT_INVOKEDYNAMIC_INFO=18;
  private final static int CONSTANT_MODULE_INFO=19;
  private final static int CONSTANT_PACKAGE_INFO=20;
  public static String getConstantType(int type){
      String result;
      switch (type){
          case CONSTANT_UTF8_INFO:
              return "UTF8";
          case CONSTANT_INTEGER_INFO:
              return "INTEGER";
          case CONSTANT_FLOAT_INFO:
              return "FLOAT";
          case CONSTANT_LONG_INFO:
              return "LONG";
          case CONSTANT_DOUBLE_INFO:
              return "DOUBLE";
          case CONSTANT_CLASS_INFO:
              return "CLASS";
          case CONSTANT_STRING_INFO:
              return "STRING";
          case CONSTANT_FIELDREF_INFO:
              return "FIELDREF";
          case CONSTANT_METHODREF_INFO:
              return "METHODREF";
          case CONSTANT_INTERFACEMETHODREF_INFO:
              return "INTERFACEMETHODREF";
          case CONSTANT_NAMEANDTYPE_INFO:
              return "NAMEANDTYPE";
          case CONSTANT_METHODHANDLE_INFO:
              return "METHODHANDLE";
          case CONSTANT_METHODTYPE_INFO:
              return "METHODTYPE";
          case CONSTANT_DYNAMIC_INFO:
              return "DYNAMIC";
          case CONSTANT_INVOKEDYNAMIC_INFO:
              return "INVOKEDYNAMIC";
          case CONSTANT_MODULE_INFO:
              return "MODULE";
          case CONSTANT_PACKAGE_INFO:
              return "PACKAGE";
          default:
              return "";
      }
  }
}
interface Constant{
  int getLength();
}
class ConstantUtf8Info implements Constant{
  private byte tag = 1;
  private int length;
  private byte[] bytes;

  public ConstantUtf8Info(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      int length = data.getBytes()[CurrentIndex+1]<<4|data.getBytes()[CurrentIndex+2];
      byte[] bytes = new byte[length];
      System.arraycopy(data.getBytes(), CurrentIndex+3,bytes,0,length);
      setLength(length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+3;
  }
  public void setLength(int length) {
      this.length = length;
  }

  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantUtf8InfoObj:tag=" + this.tag+"length="+this.length+"bytes="+new String(bytes);
  }
}

class ConstantIntegerInfo implements Constant{
  private byte tag = 3;
  private byte[] bytes = new byte[4];

  public ConstantIntegerInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantUtf8InfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1]+bytes[2]+bytes[3];
  }
}

class ConstantFloatInfo implements Constant{
  private byte tag = 4;
  private byte[] bytes = new byte[4];

  public ConstantFloatInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantUtf8InfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1]+bytes[2]+bytes[3];
  }
}
class ConstantLongInfo implements Constant{
  private byte tag = 5;
  private byte[] bytes = new byte[8];

  public ConstantLongInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantUtf8InfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1]+bytes[2]+bytes[3]+bytes[4]+bytes[5]+bytes[6]+bytes[7];
  }
}
class ConstantDoubleInfo implements Constant{
  private byte tag = 6;
  private byte[] bytes = new byte[8];

  public ConstantDoubleInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantUtf8InfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1]+bytes[2]+bytes[3]+bytes[4]+bytes[5]+bytes[6]+bytes[7];
  }
}

class ConstantClassInfo implements Constant{
  private byte tag = 7;
  private byte[] bytes = new byte[2];

  public ConstantClassInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantClassInfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1];
  }
}

class ConstantStringInfo implements Constant{
  private byte tag = 8;
  private byte[] bytes = new byte[2];

  public ConstantStringInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantStringInfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1];
  }
}

class ConstantFieldrefInfo implements Constant{
  private byte tag = 9;
  private byte[] bytes = new byte[2];
  private byte[] bytes2 = new byte[2];

  public ConstantFieldrefInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      System.arraycopy(data.getBytes(), CurrentIndex+3,bytes2,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+bytes2.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantFieldrefInfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1]+"bytes2="+bytes2[0]+bytes2[1];
  }
}

class ConstantMethodRefInfo implements Constant{
  private byte tag = 10;
  private byte[] bytes = new byte[2];
  private byte[] bytes2 = new byte[2];

  public ConstantMethodRefInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      System.arraycopy(data.getBytes(), CurrentIndex+3,bytes2,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+bytes2.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantMethodRefInfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1]+"bytes2="+bytes2[0]+bytes2[1];
  }
}

class ConstantInterfaceMethodRefInfo implements Constant{
  private byte tag = 11;
  private byte[] bytes = new byte[2];
  private byte[] bytes2 = new byte[2];

  public ConstantInterfaceMethodRefInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      System.arraycopy(data.getBytes(), CurrentIndex+3,bytes2,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+bytes2.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantInterfaceMethodRefInfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1]+"bytes2="+bytes2[0]+bytes2[1];
  }
}

class ConstantNameAndTypeInfo implements Constant{
  private byte tag = 12;
  private byte[] bytes = new byte[2];
  private byte[] bytes2 = new byte[2];

  public ConstantNameAndTypeInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      System.arraycopy(data.getBytes(), CurrentIndex+3,bytes2,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+bytes2.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantNameAndTypeInfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1]+"bytes2="+bytes2[0]+bytes2[1];
  }
}

class ConstantMethodHandleInfo implements Constant{
  private byte tag = 15;
  private byte[] bytes = new byte[1];
  private byte[] bytes2 = new byte[2];

  public ConstantMethodHandleInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      System.arraycopy(data.getBytes(), CurrentIndex+3,bytes2,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+bytes2.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantMethodHandleInfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1]+"bytes2="+bytes2[0]+bytes2[1];
  }
}

class ConstantMethodTypeInfo implements Constant{
  private byte tag = 16;
  private byte[] bytes = new byte[2];

  public ConstantMethodTypeInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantMethodTypeInfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1];
  }
}

class ConstantDynamicInfo implements Constant{
  private byte tag = 17;
  private byte[] bytes = new byte[2];
  private byte[] bytes2 = new byte[2];

  public ConstantDynamicInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      System.arraycopy(data.getBytes(), CurrentIndex+3,bytes2,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+bytes2.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantDynamicInfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1]+"bytes2="+bytes2[0]+bytes2[1];
  }
}
class ConstantInvodeDynamicInfo implements Constant{
  private byte tag = 18;
  private byte[] bytes = new byte[2];
  private byte[] bytes2 = new byte[2];

  public ConstantInvodeDynamicInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      System.arraycopy(data.getBytes(), CurrentIndex+3,bytes2,0,bytes.length);

  }
  public int getLength(){
      return bytes.length+bytes2.length+1;
  }
  @Override
  public String toString(){
      return "ConstantMethodHandleInfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1]+"bytes2="+bytes2[0]+bytes2[1];
  }
}

class ConstantModuleInfo implements Constant{
  private byte tag = 19;
  private byte[] bytes = new byte[2];

  public ConstantModuleInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      setBytes(bytes);
  }
  public int getLength(){
      return bytes.length+1;
  }
  public void setBytes(byte[] bytes) {
      this.bytes = bytes;
  }
  @Override
  public String toString(){
      return "ConstantModuleInfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1];
  }
}
class ConstantPackageInfo implements Constant{
  private byte tag = 20;
  private byte[] bytes = new byte[2];
  private byte[] bytes2 = new byte[2];

  public ConstantPackageInfo(ByteOutputStream data,int CurrentIndex) {
      if (data.getBytes()[CurrentIndex]!=this.tag){
          System.out.println("tag not match!");
          return;
      }
      System.arraycopy(data.getBytes(), CurrentIndex+1,bytes,0,bytes.length);
      System.arraycopy(data.getBytes(), CurrentIndex+3,bytes2,0,bytes.length);

  }
  public int getLength(){
      return bytes.length+bytes2.length+1;
  }
  @Override
  public String toString(){
      return "ConstantPackageInfoObj:tag=" + this.tag+"bytes="+bytes[0]+bytes[1]+"bytes2="+bytes2[0]+bytes2[1];
  }
}
class BaseAttribute{
  int attribute_name_index;
  int attribute_length;
  BaseAttribute(ByteOutputStream data,int CurrentIndex){
      byte[] accessFlag = new byte[2];
      byte[] length= new byte[4];
      System.arraycopy(data.getBytes(), CurrentIndex,accessFlag,0,accessFlag.length);
      System.arraycopy(data.getBytes(), CurrentIndex+2,length,0,length.length);
      this.attribute_length = length[0]*64|length[1]*16|length[2]*8|length[3];
      this.attribute_name_index = accessFlag[0]*4|accessFlag[1];
      //System.out.println("attribute_name_index:"+          this.attribute_name_index);
  }
  int getLength(){
      return this.attribute_length+6;
  }
  public String toString(){
      return "BaseAttributeObj:attribute_name_index=" + this.attribute_name_index+"attribute_length="+this.attribute_length;
  }
}
public class ClassAnalysis {
  private final static int CONSTANT_UTF8_INFO=1;
  private final static int CONSTANT_INTEGER_INFO=3;
  private final static int CONSTANT_FLOAT_INFO=4;
  private final static int CONSTANT_LONG_INFO=5;
  private final static int CONSTANT_DOUBLE_INFO=6;
  private final static int CONSTANT_CLASS_INFO=7;
  private final static int CONSTANT_STRING_INFO=8;
  private final static int CONSTANT_FIELDREF_INFO=9;
  private final static int CONSTANT_METHODREF_INFO=10;
  private final static int CONSTANT_INTERFACEMETHODREF_INFO=11;
  private final static int CONSTANT_NAMEANDTYPE_INFO=12;
  private final static int CONSTANT_METHODHANDLE_INFO=15;
  private final static int CONSTANT_METHODTYPE_INFO=16;
  private final static int CONSTANT_DYNAMIC_INFO=17;
  private final static int CONSTANT_INVOKEDYNAMIC_INFO=18;
  private final static int CONSTANT_MODULE_INFO=19;
  private final static int CONSTANT_PACKAGE_INFO=20;
  final static ByteOutputStream Classdata = new ByteOutputStream();
  final static ArrayList<Constant> constants = new ArrayList<Constant>();
  public static void main(String[] args) throws Exception{
      readClassFile("D:\\MyComputer\\StaticFile\\SourceCode\\SourceCode_Java\\JavaWeb\\target\\classes\\org\\controller\\defaultServlet.class");
      System.out.println("MajorVersion:"+getMajorVersion());
      System.out.println("MinorVersion:"+getMinorVersion());
      System.out.println("Constant_pool_count:"+getConstant_pool_count());
      int CurrentPos = displayConstantPool();
       CurrentPos = DisplayAccessFlag(CurrentPos);
       CurrentPos = DisplayThisClass(CurrentPos);
      CurrentPos = DisplaySuperClass(CurrentPos);
      CurrentPos = DisplayInterfaces(CurrentPos);
      CurrentPos = DisplayFields(CurrentPos);
      CurrentPos = DisplayMethods(CurrentPos);

  }
  public static int DisplayAccessFlag(int pos){
      byte[] accessFlag = new byte[2];
      System.arraycopy(Classdata.getBytes(), pos,accessFlag,0,accessFlag.length);
      int flag = accessFlag[0]*4|accessFlag[1];
      System.out.println("Access flag:0x"+          Integer.toHexString(flag));
      return pos+2;
  }
  public static int DisplayThisClass(int pos){
      byte[] buffer = new byte[2];
      System.arraycopy(Classdata.getBytes(), pos,buffer,0,buffer.length);
      int index = buffer[0]*4|buffer[1];
      System.out.println("This Class index:"+ index +" value:"+constants.get(index-1));
      return pos+2;
  }
  public static int DisplaySuperClass(int pos){
      byte[] buffer = new byte[2];
      System.arraycopy(Classdata.getBytes(), pos,buffer,0,buffer.length);
      int index = buffer[0]*4|buffer[1];
      System.out.println("Super Class index:"+ index +" value:"+constants.get(index-1));
      return pos+2;
  }
  public static int DisplayInterfaces(int pos){
      byte[] buffer = new byte[2];
      System.arraycopy(Classdata.getBytes(), pos,buffer,0,buffer.length);
      int count = buffer[0]*4|buffer[1];
      for(int i=0;i<count;i++)
      {
          System.arraycopy(Classdata.getBytes(), pos,buffer,0,buffer.length);
          int index = buffer[0]*4|buffer[1];
          System.out.println("interface Class index:"+ index +" value:"+constants.get(index-1));
      }

      return pos+2+count*2;
  }
  public static int DisplayFields(int pos){
      byte[] buffer = new byte[2];
      int CurrentPos = pos;
      System.arraycopy(Classdata.getBytes(), pos,buffer,0,buffer.length);
      CurrentPos +=2;
      int count = buffer[0]*4|buffer[1];
      System.out.println("Fields count:"+count);
      for(int i=0;i<count;i++)
      {
          byte[] access_flag=new byte[2];
          byte[] name_index=new byte[2];
          byte[] descriptor_Index=new byte[2];
          byte[] attributes_count=new byte[2];
          System.arraycopy(Classdata.getBytes(), CurrentPos,access_flag,0,access_flag.length);
          CurrentPos +=2;
          System.arraycopy(Classdata.getBytes(), CurrentPos,name_index,0,name_index.length);
          CurrentPos +=2;
          System.arraycopy(Classdata.getBytes(), CurrentPos,descriptor_Index,0,descriptor_Index.length);
          CurrentPos +=2;
          System.arraycopy(Classdata.getBytes(), CurrentPos,attributes_count,0,attributes_count.length);
          CurrentPos +=2;
          int attributeCount = attributes_count[0]*4|attributes_count[1];
          System.out.println("Fields: access_flag:"+access_flag[0]+access_flag[1]+"nameindex:"+name_index[0]+name_index[1]+"descriptorIndex:"+descriptor_Index[0]+descriptor_Index[1]+"attributesCount:"+attributeCount);
          for(int c=0;c<attributeCount;c++){
              BaseAttribute baseAttribute = new BaseAttribute(Classdata, CurrentPos);
              System.out.println(baseAttribute);
              CurrentPos+=baseAttribute.getLength();
          }

      }

      return CurrentPos;
  }
  public static int DisplayMethods(int pos){
      byte[] buffer = new byte[2];
      int CurrentPos = pos;
      System.arraycopy(Classdata.getBytes(), pos,buffer,0,buffer.length);
      CurrentPos +=2;
      int count = buffer[0]*4|buffer[1];
      System.out.println("Methods count:"+count);
      for(int i=0;i<count;i++)
      {
          byte[] access_flag=new byte[2];
          byte[] name_index=new byte[2];
          byte[] descriptor_Index=new byte[2];
          byte[] attributes_count=new byte[2];
          System.arraycopy(Classdata.getBytes(), CurrentPos,access_flag,0,access_flag.length);
          CurrentPos +=2;
          System.arraycopy(Classdata.getBytes(), CurrentPos,name_index,0,name_index.length);
          CurrentPos +=2;
          System.arraycopy(Classdata.getBytes(), CurrentPos,descriptor_Index,0,descriptor_Index.length);
          CurrentPos +=2;
          System.arraycopy(Classdata.getBytes(), CurrentPos,attributes_count,0,attributes_count.length);
          CurrentPos +=2;
          int attributeCount = attributes_count[0]*4|attributes_count[1];
          System.out.println("Method: access_flag:"+access_flag[0]+access_flag[1]+";nameindex:"+name_index[0]+name_index[1]+";descriptorIndex:"+descriptor_Index[0]+descriptor_Index[1]+";attributesCount:"+attributeCount);
          for(int c=0;c<attributeCount;c++){
              BaseAttribute baseAttribute = new BaseAttribute(Classdata, CurrentPos);
              System.out.println(baseAttribute);
              CurrentPos+=baseAttribute.getLength();
          }

      }

      return CurrentPos;
  }
  public static void readClassFile(String path) throws Exception{
      File file = new File(path);
      FileInputStream fileInputStream = new FileInputStream(file);
      byte[] buffer = new byte[1024];
      int readSize=-1;
      while((readSize = fileInputStream.read(buffer))!=-1){
          Classdata.write(buffer,0,readSize);
      }
  }
  public static int getMajorVersion() throws Exception{
      byte[] bytes = Classdata.getBytes();
      int MajorVersion = bytes[6]*10 | bytes[7];
      return MajorVersion;
  }
  public static int getMinorVersion() throws Exception{
      byte[] bytes = Classdata.getBytes();
      int MajorVersion = bytes[4]*10 | bytes[5];
      return MajorVersion;
  }
  public static int getConstant_pool_count() throws Exception{
      byte[] bytes = Classdata.getBytes();
      int MajorVersion = bytes[8]*10 | bytes[9];
      return MajorVersion;
  }
  public static int displayConstantPool() throws Exception{
      int constantCount = getConstant_pool_count();
      int CurrentIndex=10;
      Constant cons;


      for(int i=1;i<constantCount;i++){
          switch (Classdata.getBytes()[CurrentIndex]){
              case CONSTANT_UTF8_INFO:
                  cons = new ConstantUtf8Info(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_INTEGER_INFO:
                  cons = new ConstantIntegerInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_FLOAT_INFO:
                  cons = new ConstantFloatInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_LONG_INFO:
                  cons = new ConstantLongInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_DOUBLE_INFO:
                  cons = new ConstantDoubleInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_CLASS_INFO:
                  cons = new ConstantClassInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_STRING_INFO:
                  cons = new ConstantStringInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_FIELDREF_INFO:
                  cons = new ConstantFieldrefInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_METHODREF_INFO:
                  cons = new ConstantMethodRefInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_INTERFACEMETHODREF_INFO:
                  cons = new ConstantInterfaceMethodRefInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_NAMEANDTYPE_INFO:
                  cons = new ConstantNameAndTypeInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_METHODHANDLE_INFO:
                  cons = new ConstantMethodHandleInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_METHODTYPE_INFO:
                  cons = new ConstantMethodTypeInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_DYNAMIC_INFO:
                  cons = new ConstantDynamicInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_INVOKEDYNAMIC_INFO:
                  cons = new ConstantInvodeDynamicInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_MODULE_INFO:
                  cons = new ConstantModuleInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
              case CONSTANT_PACKAGE_INFO:
                  cons = new ConstantPackageInfo(Classdata, CurrentIndex);
                  constants.add(cons);
                  CurrentIndex +=cons.getLength();
                  break;
          }
      }
      for(int count = 0;count<constants.size();count++){
          System.out.println(count+1 + "     "+constants.get(count));
      }
      return CurrentIndex;
  }
}

字节码简介

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,opcode)以及跟随其后的零至多个代表此操作所需的参数(称为操作数,Operand)构成。
由于限制了Java虚拟机操作码的长度为一个字节,这意味着指令集的操作码总数不能超过256条;又由于Class文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机在处理那些超过一个字节的数据时,不得不在运行时从字节中重建出具体数据的结构,譬如要将一个16位长度的无符号整数使用两个无符号字节存储起来(假设将它们命名位byte1和byte2),那它们的值应该是这样的:
(byte1 << 8) | byte2

字节码与数据类型

在 Java 虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如,iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据。这两条指令的操作在虚拟机内部可能会是由同一段代码来实现的,但在 Class 文件中它们必须拥有各自独立的操作码。
对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i 代表对 int 类型的数据操作,l 代表 long,s 代表 short,b 代表 byte,c 代表 char,f 代表 float,d 代表 double,a 代表 reference。也有一些指令的助记符中没有明确地指明操作类型的字母,如 arraylength 指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。还有另外一些指令,如无条件跳转指令 goto 则是与数据类型无关的。
由于 Java 虚拟机的操作码长度只有一个字节,所以包含了数据类型的操作码就为指令集的设计带来了很大的压力:如果每一种与数据类型相关的指令都支持 Java 虚拟机所有运行时数据类型的话,那指令的数量恐怕就会超出一个字节所能表示的数量范围了。因此,Java 虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它,换句话说,指令集将会故意被设计成非完全独立的(Java 虚拟机规范中把这种特性称为 “Not Orthogonal”,即并非每种数据类型和每一种操作都有对应的指令)。有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。
下图列举了 Java 虚拟机所支持的与数据类型相关的字节码指令:

操作码/类型 byte short int long float double char reference
Tipush bipush sipush
Tconst iconst lconst fconst dconst aconst
Tload iload lload fload dload aload
Tstore istore lstore fstore dstore astore
Tinc iinc
Taload baload saload iaload laload faload daload caload aaload
Tastore bastore sastore iastore lastore fastore dastore castore aastore
Tadd iadd ladd fadd dadd
Tsub isub lsub fsub dsub
Tmul imul lmul fmul dmul
Tdiv idiv ldiv fdiv ddiv
Trem irem lrem frem drem
Tneg ineg lneg fneg dneg
Tshl ishl lshl
Tshr ishr lshr
Tushr iushr lushr
Tand iand land
Tor ior lor
Txor ixor lxor
i2T i2b i2s i2l i2f i2d
l2T l2i l2f l2d
f2T f2i f2l f2d
d2T d2i d2l d2f
Tcmp lcmp
Tcmpl fcmpl dcmpl
Tcmpg fcmpg dcmpg
if_TcmpOP if_icmpOP if_acmpOP
Treturn ireturn lreturn freturn dreturn areturn

从上图可以看出,大部分的指令都没有支持整数类型 byte、char 和 short,甚至没有任何指令支持 boolean 类型。编译器会在编译器或运行期将byte 和 short 类型的数据带符号扩展(Sign-Extend)为相应的int类型数据,将boolean 和 char 类型数据零位扩展(Zero-Extend)为相应的int 类型数据。与之类似,在处理 boolean、byte、short 和 char 类型的数组时,也会转换为使用对应的 int 类型的字节码指令来处理。因此,大多数对于 boolean、byte、short 和 char 类型数据的操作,实际上都是使用相应的 int 类型作为运算类型(Computational Type)。

加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容:

  • 将一个局部变量加载到操作栈:iload、iload_、lload、lload_、fload、fload_、dload、dload_、aload、aload_。
  • 将一个数值从操作数栈存储到局部变量表:istore、istore_、lstore、lstore_、fstore、fstore_、dstore、dstore_、astore、astore_。
  • 将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_、lconst_、fconst_、dconst_。
  • 扩充局部变量的访问索引的指令:wide。

存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作,除此之外,还有少量指令,如访问对象的字段或数组元素的指令也会向操作数栈传输数据。
上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如 iload_),这些指令助记符实际上是代表了一组指令(例如 iload_,它代表了 iload_0、iload_1、iload_2 和 iload_3 这几条指令)。这几组指令都是某个带有一个操作数的通用指令(例如 iload)的特殊形式,对于这若干组特殊指令来说,它们省略掉了显示的操作数,不需要进行取操作数的动作,实际上操作数就隐含在指令中。除了这点之外,它们的语义与原生的通用指令完全一致(例如 iload_0 的语义与操作数为 0 时的 iload 指令语义完全一致)。这种指令表示方法在这里以及《Java 虚拟机规范》中都是通用的。

运算指令

运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。大体上算术指令可以分为两种:对整型数据进行运算的指令与对浮点型数据进行运算的指令,无论是哪种算术指令,都使用 Java 虚拟机的数据类型,由于没有直接支持 byte、short、char 和 boolean 类型的算术指令,对于这类数据的运算,应使用操作 int 类型的指令代替。整数与浮点数的算术指令在溢出和被零除的时候也有各自不同的行为表现,所有的算术指令如下。

  • 加法指令:iadd、ladd、fadd、dadd。
  • 减法指令:isub、lsub、fsub、dsub。
  • 乘法指令:imul、lmul、fmul、dmul。
  • 除法指令:idiv、ldiv、fdiv、ddiv。
  • 求余指令:irem、lrem、frem、drem。
  • 取反指令:ineg、lneg、fneg、dneg。
  • 位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
  • 按位或指令:ior、lor。
  • 按位与指令:iand、land。
  • 按位异或指令:ixor、lxor。
  • 局部变量自增指令:iinc。
  • 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。

类型转换指令

类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作,或者用来处理本节开篇所提到的字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
Java 虚拟机直接支持(即转换时无需显示的转换指令)以下数值类型的宽化类型转换(Widening Numeric Conversions,即小范围类型向大范围类型的安全转换):

  • int 类型到 long、float 或者 double 类型。
  • long 类型到 float、double 类型。
  • float 类型到 double 类型。

相对的,处理窄化类型转换(Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成,这些转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,转换过程很可能会导致数值的精度丢失。
在将 int 或 long 类型窄化转换为整数类型 T 的时候,转换过程仅仅是简单地丢弃除最低位 N 个字节以外的内容,N 是类型 T 的数据类型长度,这将可能导致转换结果与输入值有不同的正负号。这点很容易理解,因为原来符号位处于数值的最高位,高位被丢弃之后,转换结果的符号就取决于低 N 个字节的首位了。
在将一个浮点值窄化转换为整数类型 T(T 限于int 或 long 类型之一)的时候,将遵循以下转换规则:

  • 如果浮点值是 NaN,那转换结果就是 int 或 long 类型的 0。
  • 如果浮点值不是无穷大的话,浮点值使用 IEEE 754 的向零舍入模式取整,获得整数值 v,如果 v 在目标类型 T(int 或 long)的表示范围之内,那转换结果就是 v。
  • 否则,将根据 v 的符号,转换为 T 所能表示的最大或者最小整数。

对象创建与访问指令

虽然类实例和数组都是对象,但 Java 虚拟机对类实例和数组的创建与操作使用了不同的字节码指令(数组和普通类型创建过程是不同的)。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素,这些指令如下。

  • 创建类实例的指令:new。
  • 创建数组的指令:newarray、anewarray、multianewarray。
  • 访问类字段(static 字段,或者成为类变量)和实例字段(非 static 字段,或者成为实例变量)的指令:getfield、putfield、getstatic、putstatic。
  • 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
  • 将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
  • 取数组长度的指令:arraylength。
  • 检查类实例类型的指令:instanceof、checkcast。

操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,Java 虚拟机提供了一些用于直接操作操作数栈的指令,包括:

  • 将操作数栈的栈顶一个或两个元素出栈:pop、pop2。
  • 复制栈顶一个或两个数值并将复制值或双份的复制重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
  • 将栈最顶端的两个数值互换:swap。

控制转移指令

控制转移指令可以让 Java 虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改 PC 寄存器的值。控制转移指令如下:

  • 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne。
  • 复合条件分支:tableswitch、lookupswitch。
  • 无条件分支:goto、goto_w、jsr、jsr_w、ret。

在 Java 虚拟机中有专门的指令集用来处理 int 和 reference 类型的条件分支比较操作,为了可以无须明显标识一个实体值是否 null,也有专门的指令用来检测 null 值。
与前面算术运算是的规则一致,对于 boolean 类型、byte 类型、char 类型和 short 类型的条件分支比较操作,都是使用 int 类型的比较指令来完成,而对于 long 类型、float 类型和 double 类型的条件分支比较操作,则会先执行相应类型的比较运算指令(dcmpg、dcmpl、fcmpg、fcmpl、lcmp),运算指令会返回一个整形值到操作数栈中,随后再执行 int 类型的条件分支比较操作来完成整个分支跳转。由于各种类型的比较最终都会转化为 int 类型的比较操作,int 类型比较是否方便完善就显得尤为重要,所以 Java 虚拟机提供的 int 类型的条件分支指令是最为丰富和强大的。

方法调用和返回指令

方法调用(分派、执行过程),先列举以下 5 条用于方法调用的指令:

  • invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。
  • invokeinterface 指令用于调用接口方法,它会在运行时搜索一下实现了这个接口方法的对象,找出适合的方法进行调用。
  • invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
  • invokestatic 指令用于调用类方法(static 方法)。
  • invokedynamic 指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面 4 条调用指令的分派逻辑都固化在 Java 虚拟机内部,而 invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的。

方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括 ireturn(当返回值是 boolean、byte、char、short 和 int 类型时使用)、lreturn、freturn、dreturn 和 areturn,另外还有一条return 指令共声明为 void 的方法、实例初始化方法以及类和接口的类初始化方法使用。

异常处理指令

在 Java 程序中显式抛出异常的操作(throw 语句)都由 athrow指令来实现,除了用 throw 语句显式抛出异常情况之外,Java 虚拟机规范还规定了许多运行时异常会在其他 Java 虚拟机指令检测到异常状况时自动抛出。例如,在前面介绍的整数运算中,当除数为零时,虚拟机会在 idiv 或 ldiv 指令中抛出 ArithmeticException 异常。

而在 Java 虚拟机中,处理异常(catch 语句)不是由字节码指令来实现的(很久之前曾经使用 jsr 和 ret 指令来实现,现在已经不用了),而是采用异常表来完成的。

同步指令

Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。

方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 方法标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。

同步一段指令集序列通常是由 Java 语言中的 synchronized 语句块来表示的,Java 虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义,正确实现 synchronized 关键字需要 javac 编译器与 Java 虚拟机两者共同协作支持。

公有设计和私有实现

Java虚拟机规范描绘了Java虚拟机应有的共同程序存储格式:Class文件格式以及字节码指令集。这些内容与硬件、操作系统及具体的Java虚拟机实现之间是完全独立的,虚拟机实现者可能更愿意把他们看作是程序在各种Java平台实现之间互相安全的交互的手段。

理解公有设计与私有实现之间的分界线是非常有必要的,Java虚拟机实现必须能够读取Class文件并精确实现包含在其中的Java虚拟机代码的语义。拿着Java虚拟机规范一成不变的逐字实现其中要求的内容当然是一种可行的途径,但一个优秀的虚拟机实现,在满足虚拟机规范的约束下对具体实现做出修改和优化也是完全可行的,并且虚拟机规范中明确鼓励实现者这样做。只要优化后Class文件依然可以被正确读取,并且包含在其中的语义能得到完整的保持,那实现者就可以选择任何方式去实现这些语义,虚拟机后台如何处理Class文件完全是实现者自己的事情,只要他在外部接口上看起来与规范描述的一致即可。

虚拟机实现者可以使用这种伸缩性来让Java虚拟机获得更高的性能、更低的内存消耗或者更好的可移植性,选择哪种特性取决于Java虚拟机实现的目标和关注点是什么。虚拟机实现的方式主要有以下两种:

将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集。

将输入的Java虚拟机代码在加载或执行时翻译成宿主CPU的本地指令集(即即时编译器代码生成技术)。

精确定义的虚拟机和目标文件格式不应当对虚拟机实现者的创造性产生太多的限制,Java虚拟机因被设计成可以允许有众多不同的实现,并且各种实现可以在保持兼容性的同时提供不同的、新的、有趣的解决方案。