这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」
class文件格式解析(ClassFileFormat)
给class文件内部结构每个位置代表什么、各个部分的长度等格式是严格规定死的
u1、u2、u4、u8代表几个字节的无符号数,在反编译出来的16进制文件中,两个数字代表一个字节,也就是u1
接下来我们来对这一段16进制编码进行解读
magic:
u4,魔数,代表本文件是.class文件。.jpg等也会有这种魔数,正因为魔数存在,即使将*.jpg改成*.123,也能照常打开。minor version、major version:
各u2,版本号,向下兼容,即高版本JDK可以使用低版本的.class文件,反之不行。constant_pool_count:
u2,常量池中常量的数量,0010代表有15个。constant_pool:
表类型数据集合,常量池长度=constant_pool_count-1,即常量池中每一项常量都是一个表,共有11种结构各不相同的表结构数据。这11种表都有一个共同的特点,即均由一个u1类型的标志位开始,可以通过这个标志位来判断这个常量属于哪种常量类型,常量类型及其数据结构如下表所示:
access_flags:
u2,access_flags访问标志的主要目的是标记该类是类还是接口,如果是类,访问权限是否为public,是否是abstract,是否被标志为final等,见下表:
Flag_name | Value | Interpretation |
---|---|---|
ACC_PUBLIC | 0x0001 | 表示访问权限为public,可以从本包外访问 |
ACC_FINAL | 0x0010 | 表示由final修饰,不允许有子类 |
ACC_SUPER | 0x0020 | 较为特殊,表示动态绑定直接父类,见下面的解释 |
ACC_INTERFACE | 0x0200 | 表示接口,非类 |
ACC_ABSTRACT | 0x0400 | 表示抽象类,不能实例化 |
ACC_SYNTHETIC | 0x1000 | 表示由synthetic修饰,不在源代码中出现 |
ACC_ANNOTATION | 0x2000 | 表示是annotation类型 |
ACC_ENUM | 0x4000 | 表示是枚举类型 |
而在access_flags的标记就只有2个字节的长度,如何装下这么多的符号呢?
其实他在保存多符号的时候会先进行与运算这样的话就能够用2个字节,使用多个flag
注:access_flags 出现在class文件中的类的层面上, 那么它只能描述类型的修饰符, 而不能描述字 段或方法的修饰符, 不要将这里的access_flags 和后面要介绍的方法表和字段表中的访问修饰符相 混淆
-
this_class:
u2,类索引,用于确定这个类的全限定名,2个字节是指向常量池的地址 -
super_class:
u2,父类索引,用于确定这个类父类的全限定名(Java语言不允许多重继承,故父类索引只有一个。除了java.lang.Object类之外所有类都有父类,故除了java.lang.Object类之外,所有类该字段值都不为0),占2字节 -
interfaces_count:
u2,接口索引计数器。如果该类没有实现任何接口,则该计数器值为0,并且后面的接口的索引集合将不占用任何字节。 -
interfaces:
接口索引集合,一组u2类型数据的集合。用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果该类本身为接口,则为extends语句)后的接口顺序从左至右排列在接口的索引集合中 -
fields_count:
u2,字段表计数器,即字段表集合中的字段表数据个数。,其值为0x0001,即只有一个字段表数据,也就是测试类中只包含一个变量**(不算方法内部变量)** -
fields:
每个u8c长度的字段表集合,一组字段表类型数据的集合。字段表用于描述接口或类中声明的变量,包括类级别(static)和实例级别变量,不包括在方法内部声明的变量
在Java中一般通过如下几项描述一个字段:字段作用域(public、protected、private修饰符)、是类级别变量还是实例级别变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、可序列化与否(transient修饰符)、字段数据类型(基本类型、对象、数组)以及字段名称。在字段表中,变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示,字段表格式如下表所示:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
注:在filds
的中所标识的access_flags所对应的就是上方图表 点击查看
methods_count:
u2,方法表计数器,即方法表集合中的方法表数据个数。其值为0x0002,即测试类中有2个方法
methods:
方法表集合,一组方法表类型数据的集合。方法表结构和字段表结构一样:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
attributes_count:
u2,属性计数器,(0x0000,所以该字段没有额外需要描述的信息)attributes:
在Class文件、属性表、方法表中都可以包含自己的属性表集合,用于描述某些场景的专有信息与Class文件中其它数据项对长度、顺序、格式的严格要求不同,属性表集合不要求其中包含的属性表具有严格的顺序,并且只要属性的名称不与已有的属性名称重复,任何人实现的编译器可以向属性表中写入自己定义的属性信息。虚拟机在运行时会忽略不能识别的属性,为了能正确解析Class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类文件、字段表、方法表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTale | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类文件、方法表、字段表 | 标识方法或字段是由编译器自动生成的 |
每种属性均有各自的表结构。这9种表结构有一个共同的特点,即均由一个u2类型的属性名称开始,可以通过这个属性名称来判段属性的类型
Code属性:Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在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 |
max_stack:操作数栈深度最大值,在方法执行的任何时刻,操作数栈深度都不会超过这个值。虚拟机运行时根据这个值来分配栈帧的操作数栈深度
max_locals:局部变量表所需存储空间,单位为Slot(参见备注四)。并不是所有局部变量占用的Slot之和,当一个局部变量的生命周期结束后,其所占用的Slot将分配给其它依然存活的局部变量使用,按此方式计算出方法运行时局部变量表所需的存储空间
code_length和code:用来存放Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。
每一个指令是一个u1类型的单字节,当虚拟机读到code中的一个字节码(一个字节能表示256种指令,Java虚拟机规范定义了其中约200个编码对应的指令),就可以判断出该字节码代表的指令,指令后面是否带有参数,参数该如何解释,虽然code_length占4个字节,但是Java虚拟机规范中限制一个方法不能超过65535条字节码指令,如果超过,Javac将拒绝编译
ConstantValue属性:通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量(类变量)才可以使用这项属性。其结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | constantvalue_index | 1 |
可以看出ConstantValue属性是一个定长属性,其中attribute_length的值固定为0x00000002,constantvalue_index为一常量池字面量类型常量索引(Class文件格式的常量类型中只有与基本类型和字符串类型相对应的字面量常量,所以ConstantValue属性只支持基本类型和字符串类型)
对非static类型变量(实例变量,如:int a = 123;)的赋值是在实例构造器方法中进行的
对类变量(如:static int a = 123;)的赋值有2种选择,在类构造器方法中或使用ConstantValue属性。当前Javac编译器的选择是:如果变量同时被static和final修饰(虚拟机规范只要求有ConstantValue属性的字段必须设置ACC_STATIC标志,对final关键字的要求是Javac编译器自己加入的要求),并且该变量的数据类型为基本类型或字符串类型,就生成ConstantValue属性进行初始化;否则在类构造器方法中进行初始化
Exceptions属性:列举出方法中可能抛出的受查异常(即方法描述时throws关键字后列出的异常),与Code属性平级,与Code属性包含的异常表不同,其结构为:
类型 | 名称 | 数量 |
---|---|---|
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为异常索引集合,一组u2类型exception_index的集合,每一个exception_index为一个指向常量池中一CONSTANT_Class_info型常量的索引,代表该受查异常的类型
InnerClasses属性:该属性用于记录内部类和宿主类之间的关系。如果一个类中定义了内部类,编译器将会为这个类与这个类包含的内部类生成InnerClasses属性,结构为:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_classes | 1 |
inner_classes_info | inner_classes | number_of_classes |
inner_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类型常量索引,该CONSTANT_Class_info类型常量指向常量池中CONSTANT_Utf8_info类型常量,分别为内部类的全限定名和宿主类的全限定名
inner_name_index指向常量池中CONSTANT_Utf8_info类型常量的索引,为内部类名称,如果为匿名内部类,则该值为0
inner_name_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 | 0x0020 | 内部类是否为一个接口 |
ACC_ABSTRACT | 0x0400 | 内部类是否为abstract |
ACC_SYNTHETIC | 0x1000 | 内部类是否为编译器自动产生 |
ACC_ANNOTATION | 0x4000 | 内部类是否是一个注解 |
ACC_ENUM | 0x4000 | 内部类是否是一个枚举 |
LineNumberTale属性:用于描述Java源码的行号与字节码行号之间的对应关系,非运行时必需属性,会默认生成至Class文件中,可以使用Javac的-g:none或-g:lines关闭或要求生成该项属性信息,其结构如下:
类型 | 名称 | 数量 |
---|---|---|
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_info类型数据的集合,其所包含的line_number_info类型数据的数量为line_number_table_length,line_number_info结构如下:
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | start_pc | 1 | 字节码行号 |
u2 | line_number | 1 | Java源码行号 |
不生成该属性的最大影响是:1,抛出异常时,堆栈将不会显示出错的行号;2,调试程序时无法按照源码设置断点
LocalVariableTable属性:用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,非运行时必需属性,默认不会生成至Class文件中,可以使用Javac的-g:none或-g:vars关闭或要求生成该项属性信息,其结构如下:
类型 | 名称 | 数量 |
---|---|---|
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_table是一组local_variable_info类型数据的集合,其所包含的local_variable_info类型数据的数量为local_variable_table_length,local_variable_info结构如下:
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | start_pc | 1 | 局部变量的生命周期开始的字节码偏移量 |
u2 | length | 1 | 局部变量作用范围覆盖的长度 |
u2 | name_index | 1 | 指向常量池中CONSTANT_Utf8_info类型常量的索引,局部变量名称 |
u2 | descriptor_index | 1 | 指向常量池中CONSTANT_Utf8_info类型常量的索引,局部变量描述符 |
u2 | index | 1 | 局部变量在栈帧局部变量表中Slot的位置,如果这个变量的数据类型为64位类型(long或double),它占用的Slot为index和index+1这2个位置 |
start_pc + length即为该局部变量在字节码中的作用域范围
不生成该属性的最大影响是:1,当其他人引用这个方法时,所有的参数名称都将丢失,IDE可能会使用诸如arg0、arg1之类的占位符代替原有的参数名称,对代码运行无影响,会给代码的编写带来不便;2,调试时调试器无法根据参数名称从运行上下文中获取参数值
SourceFile属性:用于记录生成这个Class文件的源码文件名称,为可选项,可以使用Javac的-g:none或-g:source关闭或要求生成该项属性信息,其结构如下:
型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_index | 1 |
可以看出SourceFile属性是一个定长属性,sourcefile_index是指向常量池中一CONSTANT_Utf8_info类型常量的索引,常量的值为源码文件的文件名
对大多数文件,类名和文件名是一致的,少数特殊类除外(如:内部类),此时如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错误代码所属的文件名
**Deprecated属性和Synthetic属性:**这两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念
Deprecated属性表示某个类、字段或方法已经被程序作者定为不再推荐使用,可在代码中使用@Deprecated注解进行设置
Synthetic属性表示该字段或方法不是由Java源码直接产生的,而是由编译器自行添加的(当然也可设置访问标志中的ACC_SYNTHETIC标志,所有由非用户代码产生的类、方法和字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器和类构造器方法)
这两项属性的结构为(当然attribute_length的值必须为0x00000000):
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
PS:
1,全限定名:将类全名中的“.”替换为“/”,为了保证多个连续的全限定名之间不产生混淆,在最后加上“;”表示全限定名结束。例如:"com.test.Test"类的全限定名为"com/test/Test;"
2,简单名称:没有类型和参数修饰的方法或字段名称。例如:"public void add(int a,int b){...}"该方法的简单名称为"add","int a = 123;"该字段的简单名称为"a"
3,描述符:描述字段的数据类型、方法的参数列表(包括数量、类型和顺序)和返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符表示,而对象类型则用字符L加对象全限定名表示
标识字符 | 含义 |
---|---|
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型,如:Ljava/lang/Object; |
对于数组类型,每一维将使用一个前置的“[”字符来描述,如:"int[]"将被记录为"[I","String[][]"将被记录为"[[Ljava/lang/String;"
用描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组"()"之内,如:方法"String getAll(int id,String name)"的描述符为"(I,Ljava/lang/String;)Ljava/lang/String;"
4,Slot,虚拟机为局部变量分配内存所使用的最小单位,长度不超过32位的数据类型占用1个Slot,64位的数据类型(long和double)占用2个Slot