Class类文件是虚拟机中立特性的基石,虚拟机中立特性包括:
平台无关性:虚拟机提供商发布了各种可以在不同平台上运行的虚拟机,这些虚拟机统一运行一种与平台无关的程序存储文件即字节码文件。
语言无关性:虚拟机不和包括Java在内的任何语言绑定,只关联Class类文件这种特殊的二进制格式文件,任何语言都可以表示为Class类文件(如Groovy程序被groovyc编译器编译成Class文件)从而在虚拟机上运行。
每个Class文件都唯一对应一个类或接口的定义信息,但类或接口不一定都得定义在文件里(类或接口也可以通过类加载器直接生成)。
Class文件是一组以字节为单位的二进制流,各数据项严格按照顺序紧凑地排列在Class文件中,中间没有任何分隔符。遇到一个字节以上的数据项时,按照高位在前的方式分割为多个字节进行存储。Class文件采用一种类似于C语言结构体的伪结构来存储数据,支持无符号数和表两种数据类型。无符号数(u1,代表一个字节的无符号数)属于基本数据类型,描述数字、引用、或UTF-8编码格式的字符串 。表(一般以_info结尾)是由无符号数和其他表作为数据项构成的复合数据类型,描述有层次关系的数据,整个Class文件本质就是一张表。无论是无符号数还是表,使用前置计数器加若干数据项的形式描述同一类型的数据集合。
表中的数据项,无论是顺序还是数量,包括数据存储的字节序(Class文件为Big-Endian,最高位字节在地址最低位,最低位字节在地址最高位)都是被严格限定的。
如果把Java程序中的信息分为代码(Code,方法体里面的代码)和元数据(Metadata,包括类、字段、方法定义等其他信息),那么在整个Class文件中,attribute_info中的Code属性用于描述代码,其他数据项用于描述元数据。
javap是一个专门用于分析Class字节码的工具,位于JDK的bin目录中。
[10256170@zte.intra@LIN-5FF84753572 demo]$ javap -help
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
[10256170@zte.intra@LIN-5FF84753572 demo]$ javap -v IndexController
警告: 二进制文件IndexController包含com.example.demo.IndexController
Classfile /media/vdb1/10256170/upload/myspringboot/target/classes/com/example/demo/IndexController.class
Last modified 2021-6-3; size 602 bytes
MD5 checksum ace801fb6b4fe8785df26e00beb15a95
Compiled from "IndexController.java"
public class com.example.demo.IndexController
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#21 // java/lang/Object."<init>":()V
#2 = String #22 // hello, there
#3 = Class #23 // com/example/demo/IndexController
#4 = Class #24 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 Lcom/example/demo/IndexController;
#12 = Utf8 index
#13 = Utf8 ()Ljava/lang/String;
#14 = Utf8 RuntimeVisibleAnnotations
#15 = Utf8 Lorg/springframework/web/bind/annotation/RequestMapping;
#16 = Utf8 value
#17 = Utf8 /
#18 = Utf8 SourceFile
#19 = Utf8 IndexController.java
#20 = Utf8 Lorg/springframework/web/bind/annotation/RestController;
#21 = NameAndType #5:#6 // "<init>":()V
#22 = Utf8 hello, there
#23 = Utf8 com/example/demo/IndexController
#24 = Utf8 java/lang/Object
{
public com.example.demo.IndexController();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/demo/IndexController;
public java.lang.String index();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: ldc #2 // String hello, there
2: areturn
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this Lcom/example/demo/IndexController;
RuntimeVisibleAnnotations:
0: #15(#16=[s#17])
}
SourceFile: "IndexController.java"
RuntimeVisibleAnnotations:
0: #20()
[10256170@zte.intra@LIN-5FF84753572 demo]$
1、魔数与Class文件的版本
Class文件的头4个字节称为魔数(Magic Number),标识该文件是一个Class文件。紧接着魔数的4个字节存储Class文件的版本号,第5、6个字节存储次版本号(Minor Version),第7、8字节存储主版本号(Major Version)。JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。
2、常量池
紧接着主次版本号之后的是常量池入口,入口放置一项u2类型的数据,代表常量池的容量计数值(constant_pool_count)。Class文件结构中,只有常量池的容量计数是从1开始的(指向常量池的索引值为0时表示不引用任何一个常量池数据项),其他集合类型全都是从0开始的。
常量池中主要存放字面量(Literal)和符号引用(Symbolic References),字面量类似于Java中常量的概念,如整型、字符串、布尔型常量值和final定义的常量等。符号引用属于编译原理方面的概念,包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符三类常量。Java代码在进行Javac编译时不像C和C++一样有连接的步骤,而是在虚拟机加载Class文件的时候进行动态连接,即Class文件中不会保存字段、方法的最终内存信息,因此这些字段、方法的符号引用不经过运行期转换的话无法获得真正的内存入口地址,也就无法直接被虚拟机使用。虚拟机运行时,需要从常量池获取对应的符号引用,在类创建或运行时解析、翻译到具体的内存地址中。
常量池中的每一项常量都是一个表,表开始是一个u1类型的标志位(tag),代表当前这个常量属于哪种常量类型。
每种常量类型都有各自的结构。
3、访问标志
常量池结束后紧接着的两个字节代表访问标志(access_flags),用于识别一些类或者接口层次的访问信息。目前只使用了access_flags中的8位作为标志位,分别代表不同的含义,未被使用的标志位一律置为0。
4、类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)是u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,由这三项数据确定类的继承关系。类索引和父类索引分别用于确定类和父类的全限定名,由于Java不支持多继承,所以除了java.lang.Object类的父类索引为0外,所有类的父类索引有且只有一个。接口索引集合用于描述类实现的接口,按照程序中实现的顺序从左到右排列在集合中。
类索引、父类索引和接口索引集合按顺序排列在访问标志之后,类索引和父类索引的值指向类型为CONSTANT_Class_info的类描述符常量,通过类描述符常量的值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。接口索引集合入口的第一项为u2类型的接口计数器(interfaces_count),表示索引表的容量,计数器的值为0代表类没有实现任何接口,此时后面的索引表不占用任何字节。
5、字段表集合
字段表(field_info)用于描述接口或类中声明的变量,包括类级变量和实例级变量,不包括局部变量。
访问标志(access_flags)描述字段的修饰符,含义如下表所示:
紧接着访问标志的是名称索引(name_index)和描述符索引(descriptor_index),分别代表字段的简单名称和字段的描述符,值都是对常量池的引用。
描述符用于描述字段的数据类型、方法的参数列表(包括数量、类型、顺序)和返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符表示,对象类型则用L加对象的全限定名表示。对于数组类型,每一维度将使用一个前缀[表示,如[[Ljava/lang/String;表示二维数组String[][],[I表示整型数组int[]。描述符描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按顺序放在()中,如void inc()方法的描述符为()V,String toString()方法的描述符为()Ljava/lang/String;,int indexOf(char[] source, char[] target, int fromIndex)方法的描述符为([C[CI)I。
descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息,对于private int m,属性表计数器的值为0代表没有额外的描述信息;对于final static int m =123,将会多出一项名称为ConstantValue的属性,其值指向常量123。
字段表集合中不会列出从超类或父接口中继承而来的字段,但有可能列出Java代码中不存在的字段(如内部类指向外部类的字段)。在Java中,两个字段不管修饰符、数据类型是否相同,都不允许重名;而在字节码中,只要两个字段的描述符不一致,字段重名就是合法的。
6、方法表集合
方法表的结构同字段表一样,如下表所示:
方法表的所有标志位和取值如下表所示:
访问标志(access_flags)、名称索引(name_index)和描述符索引(descriptor_index)描述了方法的定义,方法的代码被编译器编译成字节码指令后,存放在方法属性表(attribute_info)的code属性里。
如果父类方法在子类中没有被重写(Override),方法表中就不会出现父类的方法信息,但有可能出现编译器自动添加的方法(如类构造器方法<clinit>和实例构造器方法<init>)。Java代码的方法特征签名只包括方法名称、参数顺序、参数类型,但字节码的特征签名还包括方法返回值和受查异常表。所以Java中无法紧靠返回值不同来实现方法的重载(Overload),但在Class文件中,允许存在仅返回值不同的两个同名方法。
7、属性表集合
与Class文件中其他数据项要求严格的顺序、长度和内容不同,属性表集合(attribute_info)不再要求严格的顺序,且只要不与已有的属性名重复,任何编译器都可以向属性表中写入自定义的属性信息。Java虚拟机运行时会忽略掉它不认识的属性,为了能正确解析Class文件,虚拟机预定义了如下表所示的属性。
每个属性的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,属性值的结构则是自定义的,只需要通过一个u4的长度属性去说明属性值占用的位数即可。
code属性:
Java程序方法体中的代码被编译器编译为字节码指令作为code属性的值,存储在方法表的属性集合中,方法体中没有代码(如接口中的方法、抽象类中的抽象方法)则不存在code属性。
属性名称索引(attribute_name_index)为指向常量池中CONSTANT_Utf8_info型常量的索引,常量值固定为Code,代表该属性的属性名称;
属性长度(attribute_length)代表属性值的长度,值为整个属性表的长度减去6个字节(属性名称索引和属性长度一共占6个字节);
max_stack代表操作数栈(Operand Stacks)深度的最大值,虚拟机运行时根据这个值来分配栈帧(Stack Frame)中的操作栈深度;
max_locals代表局部变量表所需的存储空间,单位是Slot(虚拟机为局部变量分配内存的最小单位),对于数据类型长度不超过32位的局部变量,占用1个Slot,而double和long两种64位的局部变量,占用2个Slot;
code_length代表Java源程序编译后生成的字节码的长度,理论最大值为2^32-1,实际上虚拟机限制一个方法不允许超出65535条字节码指令;
code存储字节码指令的一系列字节流,每个字节代表一个字节码指令;
异常表(exception_table)格式如下表所示:
含义为:当字节码在第start_pc行(包含)到end_pc行(不包含)之间出现类型为catch_type(指向CONSTANT_Class_info型常量的索引,为0代表任何异常)或其子类的异常,则转到第handler_pc行继续处理。
Exceptions属性:
列举出方法中可能抛出的受检查异常(Checked Exceptions),也就是方法描述时throws后面列举的异常,结构如下表所示:
number_of_exceptions表示方法可能抛出受检查异常的种数,每一种受检查异常用一个exception_index_table项表示。exception_index_table为指向常量池中的CONSTANT_Class_info型常量的索引,代表受检查异常的类型。
LineNumberTable属性:
描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系,不是运行时必须的属性,但默认会生成到Class文件中,可以在javac中使用-g:none或-g:lines选项要求取消或生成这项信息。
line_number_table是一个数量为line_number_table_length,类型为line_number_info的集合, line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。
LocalVariableTable属性:
描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,不是运行时必须的属性,但默认会生成到Class文件中,可以在javac中使用-g:none或-g:vars选项要求取消或生成这项信息。
start_pc和length分别代表这个局部变量生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合就是局部变量的作用域范围。name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表局部变量的名称和描述符。index代表局部变量在栈帧局部变量表中Slot的位置,当变量数据类型是64位类型(double和long)时,占用的Slot为index和index+1两个。
在JDK1.5引入泛型之后,LocalVariableTable增加了一个姐妹属性LocalVariableTypeTable,新增属性结构与LocalVariableTable相似,仅仅把记录的字段描述符的descriptor_index换成了字段的特征签名(Signature),对于非泛型类型来说,描述符和特征签名描述的信息基本一致,但是泛型引入后,描述符中泛型的参数化类型被擦除掉,就不能准确地描述泛型了。
SourceFile属性:
记录生成这个Class文件的源码名称,不是运行时必须的属性,但默认会生成到Class文件中,可以在javac中使用-g:none或-g:source选项要求取消或生成这项信息。
sourcefile_index为指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名。
ConstantValue属性:
通知虚拟机自动为静态变量赋值,只有被static修饰的变量(类变量)才可以使用这项属性。虚拟机对实例变量的赋值是在实例构造器方法<init>中进行的,而对于类变量有两种赋值方式,在类构造器方法<clinit>中赋值或使用ConstantValue属性赋值。
目前Sun编译器的选择是,如果变量同时由final和static修饰,且数据类型是基本类型或字符串的话,就生成ConstantValue属性来进行初始化;若果没有被final修饰,且不是基本类型或字符串,则在类构造器方法<clinit>中初始化。
constantvalue_index代表常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一种。
InnerClasses属性:
记录内部类与宿主类之间的关联。
number_of_classes代表内部类的个数,每一个内部类由1个inner_classes_info表进行描述。
inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_Class_info型常量的索引,分别代表内部类和宿主类的符号引用。
inner_name_index指向常量池中CONSTANT_Utf8_info型常量的索引,代表内部类的名称,如果是匿名类,值为0。
Deprecated及Synthetic属性:
标志类型,只存在有和没有,没有属性值的概念。Deprecated表示类、字段或方法已经过时,Synthetic表示字段或方法不是由Java源码产生的,而是由编译器自行添加的。
attribute_length的值必须为0x00000000,因为没有任何属性值需要设置。
StackMapTable属性:
包含零至多个栈映射帧(Stack Map Frames),每个栈映射帧显示或隐式地代表一个字节码偏移量,表示执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈需要的类型来判断字节码指令是否符合逻辑约束。
在版本号大于或等于50.0的Class文件中,如果方法的Code属性中没有StackMapTable属性,隐式地含有一个number_of_entries为0的StackMapTable属性。一个方法的Code属性最多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常。
Signature属性:
JDK1.5后的任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。Java语言的泛型是使用擦除法实现的伪泛型,编译后字节码中的泛型信息(类型变量、参数化类型)会被擦除,运行时无法通过反射获得泛型信息。Signature属性就是为了弥补这个缺陷而增设的,现在的Java反射API获取的泛型信息就来源于这个属性。
signature_index为指向常量池中CONSTANT_Utf8_info型常量的索引,表示类、方法类型或字段类型的签名。
BootstrapMethods属性:
JDK1.7后新增的属性,用于保存invokedynamic指令引用的引导方法限定符。如果类文件的常量池出现过CONSTANT_invokeDynamic_info型常量,则属性表中必须存在一个且至多一个明确的BootstrapMethods属性。
num_bootstrap_methods代表bootstrap_methods表中引导引导方法限定符的数量,bootstrap_method_ref为指向常量池中CONSTANT_MethodHandle_info型常量的索引,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中的一种。