Class文件描述
- Class文件是一组以8个字节为基础单位的二进制流
- 文件格式 采用类似于C语言结构体的伪结构存储数据,这种伪结构只有两种数据类型:无符号数、表
- 无符号数:以u1、u2、u4、u8来分别代表1个字节、2个、4个和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或按照UTF-8编码构成字符串值
- 表是由多个无符号数或其他表作为数据项构成的符合数据类型,都以“-info”结尾。表用于描述有层次关系的符合结构的数据,整个Class文件本质上可以视作一张表,由下表所示的数据项按照严格顺序排列:
- 无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,使用一个前置的容量计数器加若干个连续的数据项的形式,称这一系列连续的某一类型的数据为某一类型的“集合”。
Class文件字节码分析
- 使用下面代码,根据编译输出的.class文件进行解释
package test;
public class aaa {
private int m;
public int inc() {
return m+1;
}
}
- WinHex打开class文件:
1、魔数 Magic Number
- 头四个字节被称为魔数,作用是确定这个文件是否为一个能被虚拟机接受的class文件,java的魔数为0xCAFEBABE
2、次版本号、主版本号
- 第5、6个字节分别是次版本号和主版本号,虚拟机拒绝执行超过其版本号的class文件
- 图中主版本号的十进制是56,对应jdk12的版本号
3、常量池
- 常量池是class文件中空间最大的数据项目,也是class文件中第一个出现的表型数据项目
(1)常量池容量
- 常量池中常量的数量不是固定的,因此在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)
- 与java语言习惯不同,这个容量计数从1开始,而不是从0开始,目的在于如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义时,可以把索引值设置为0来表示,除了常量池之外的所有集合类型,都是从0开始
- 图中常量池容量0x0013,十进制为19,表示常量池容量为18个,索引为1~18
(2)常量池常量
-
常量池中每一项常量都是一个表,迄今为止共有17种不同类型常量:
-
这17类表都有一个共同点:表结构起始的第一位是一个u1类型的标志位(tag),代表当前常量属于哪种常量类型
此时对我们得到的class文件中第一个常量进行解读:
-
标志位(u2):0x0A,十进制为10,查上述的常量类型表可以得到这个常量属于CONSTANT_Methodref_info类型,表示类中方法的符号引用
-
CONSTANT_Methodref_info的结构如下:
-
tag为标志位,0x0A,10
-
u2格式的index:指向声明方法的类描述符CONSTANT_Class_info的索引项
0x0004说明指向常量池的第4项常量 -
u格式的index:指向名称及类型描述符CONSTANT_NameAndType的索引项
0x000F说明指向常量池的第16项常量 -
因此一个tag=0x0A的CONSTANT_Methodref_info类型的表数据,其总长度为tag+index+index=u1+u2+u2=5个字节的长度,加上u1的高位补零,总共是六个字节的长度,结束之后class文件中即为下一个常量的字节码
对常量池的第二个常量解读
- 第二个常量的tag:
0x09表示的常量项目类型:CONSTANT_Fieldref_info,字段的符号引用,其结构包含: - tag(u1):0x09
- index(u2):指向声明字段的类或者接口描述符CONSTANT_Class_info索引项
0x0003,即指向第三个常量 - index(u2):指向字段描述符CONSTANT_NameAndType的索引项
即指向第16个常量
第三个常量解释
- tag=0x07,即CONSTANT_Class_info常量类型,其结构包括:
- tag(u1):标志位
- index(u2):指向全限定名常量项的索引
即指向第0x0011 -> 17 个常量
第四个常量tag也为7,与第三个一致
第五个常量解释
- tag为1,即CONSTANT_Utf8_info类型常量,表示UTF-8编码的字符串
- length(u2):字符串占用的字节数
长度为1 - bytes(u1):长度为length的字符串,每个字符占用一个u1
6D的十进制为109,是m的ASCII码
后续常量池的解释方法均一致,javap对class文件反向编译的常量池结果如下:
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // test/aaa.m:I
#3 = Class #17 // test/aaa
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 aaa.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 test/aaa
#18 = Utf8 java/lang/Object
4、访问标志
- 常量池结束后,紧接着的2个字节代表访问标志(access_flags),用于识别一些类或者接口层次的访问信息,具体如下:
- 以本例子为例,aaa.java是一个普通java类,不是接口、枚举、注解或者模块,被public修饰但没有被声明为final或abstract,并且使用jdk 1.2之后的编辑器编译,因此ACC_PUBLIC、ACC_SUPER标志位为真,其余均为假,因此其标志位access_flags值应为:0x0001|0x0020=0x0021
5、类索引、父类索引与接口索引集合
- 类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中这三项数据用来确定该类型的继承关系
- 类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名
- 除了java.lang.Object之外,所有的类父类索引都不为0
- 接口索引集合用来描述这个类实现了哪些接口,这些接口按照implements关键字(如果这个Class文件表示一个接口,则应当是extends)后的接口顺序从左到右排列在接口索引集合中
- 类索引和父类索引各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串
- 接口类索引集合,入口的第一项u2类型数据为接口计数器(interfaces_count),表示索引表的容量,如果没实现接口,则计数器为0
- 本class文件的三个索引字节码:
- 可以看到this_class、super_class和interfaces分别为0x0003、0x0004、0x0000,说明其类索引和父类索引分别指向前面常量池中的第3、4个常量,实现接口为0:
6、字符段集合
- 字段表(field_info)用于描述接口或类中声明的变量,java语言中的字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量
- 字段中可以包括的修饰符有:字段的作用域(public、private、protected)、是实例变量还是类变量(static)、可变性(final)、并发可见性(volatile)、可否被序列化(reansient)、字段数据类型(基本数据类型、对象、数组)、字段名称
- 上述中黄色部分修饰符是否包含是一个布尔值,要么包含、要么不包含,因此可以用标志位来表示,而字段名称、类型无法固定,需要从常量池中引用
- 一个完整的字段表如下:
①、字段修饰符放置在access_flags中,与类中的access_flags非常相似,是u2的数据类型
- 可以设置的标志位和含义如下:
- 字段修饰符之前有一个u2类型的field_count,表示字段表数据个数,本例中field_count和access_flags如下:
- field_count=0x0001,说明包含一个字段表数据,access_flags=0x0002,说明字段修饰符为private
②、跟随access_flags标志的是两项索引值:name_index和descriptor_index,都是对常量池的引用,分别代表字段的简单名称和字段和方法的描述符
- 全限定名:java/lang/Object,这种类型
- 简单名称:没有类型和参数修饰的方法和字段名称,比如类中的inc()方法和m字段的简单名称分别为“inc”和“m”
- 方法和字段的描述符:用以描述字段的数据类型、方法的参数列表(包括数量、类型及顺序)和返回值
- 字段描述符:基本数据类型以及代表无返回值的void类型都用一个大写字母表示,对象类型用L加对象的全限定名表示,数组类型每一维度将使用一个前置的“[”表示,如一个定义为“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)的描述符为"([CII[C)I",方法描述符放在后续的放发表集合中
- 本例中字段简单名称和字段的描述符实例如下:
- name_index的值为0x0005,指向第5个常量,从常量池表中查得第5个常量为CONSTANT_Utf8_info类型的字符串,其值为“m”
- descriptor_index值为0x0006,指向第6个常量,同样查得对应的CONSTANT_Utf8_info类型的字符串,值为“I”
综上,根据①和②中的字段修饰符、简单名称及字段的描述符,可以得到源代码定义的字段为“private int m”
③、本例中字段表所包含的固定数据项目到dexcriptor_Index就结束了,不过在之后跟随着一个属性表集合,用于存储一些额外的信息
- 本例中的u2类型attributes_count为0,因此不包含额外信息
- 但是如果将字段m的声明改为“final static int m=123”,就可能存在一项名称为ConstantValue的属性,其值指向常量123,关于attribute_info的其他内容,在后续8、中的属性表集合进一步说明
④、字段表集合中不会列出从父类或者父接口中继承来的字段,但可能出现原本java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字段,并且java字段定义不支持重载
7、方法表集合
- 方法表和字段表采用几乎完全一致的方式,方法表结构也与字段表完全一致
- 两者不同的地方在于访问标志和属性表集合的可选项有所区别
- volatile和transient关键字不能修饰方法,syncheonized、native、strictfp和abstract可以修饰方法,因此方法表中标志位如下:
- 方法的定义通过访问标志、名称索引、描述符索引来表达清楚,方法里的代码经过javac编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性中,属性表在下一部分进行说明
- 通过本实例来说明方法表集合:
- methods_count=0x0002,即方法表中包含两个方法,这两个方法为编译器添加的实例构造器和源码中定义的方法inc()
- access_flags=0x0001,说明第一个方法为public
- name_index=0x0007,说明第一个方法名指向第7个常量,即""
- descriptor_index=0x0008,说明第一个方法的方法描述符指向第8个常量,即”()V“
- attributes_count=0x0001,说明第一个方法的属性表集合中有一个属性
- attribute_name_index=0x0009,说明第一个方法的属性表集合中的属性名称指向第9个常量,即”Code“
- 在第一个方法定义完成后,进入第一个方法的属性表集合属性中,而不是直接进入第二个方法
与字段表集合相对应,如果父类方法没有在子类中重写,方法表集合中就不会出现来自父类的方法信息。同样的,有可能出现由编译器自动添加的方法,最常见的时类构造器"()“方法和实例构造器”()"方法
8、属性表集合
-
属性表(attribute_info)可以出现在Class文件、字段表、方法表值中,迄今为止共有29项预定义属性,按照示例代码说明几个属性表集合
-
上文方法表中第一个方法的方法描述符后即为属性表集合,可以看出其属性个数 attributes_count=0x0001,attribute_name_index=0x0009,即”Code“,因此后续字节码为第一个方法的Code属性描述,其中Code属性表结构如下:
-
可以看出,方法表的结尾和属性表的开始都是该属性的attribute_name_index
-
attribute_length=0x0000001D=29,表示Code属性的长度为29个字节,即图中阴影部分为29个字节
-
max_stack=0x0001,表示操作数栈(Operand Stack)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度,虚拟机运行时需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。
-
max_locals=0x0001,表示局部变量表所需的存储空间。javac编译器根据变量的作用域来分配变量槽给各个变量使用,并根据同时生存的最大局部变量数量和类型计算出max_locals的大小
-
code_length=0x00000005,表示Java源程序方法编译后的字节码指令长度为5
-
code=2A B7 00 01 B1,表示5个字节码指令,关于字节码指令后续说明
-
exception_table_length=0x0000,说明本方法中没有显示异常处理表
-
attributes_count=0x0001,说明Code属性表内部有一个属性表
-
attributes=0x000A,指向第十个常量,即”LineNumberTable“,其属性结构如下
-
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。不是运行时必须的属性,但默认会生成到Class文件中,本例的对应字节码如下:
-
attribute_name_index=000A,与Code结构表中末尾的属性索引一致,即”LineNumberTable“
-
attribute_length=0x00000006,表示属性描述长度为6字节
-
line_number_table_length=0x0001,表示后面的 line_number_info 表有 1 个
-
line_number_info表包括了 start_pc 和 line_number 两个 u2 类型的数据项,前者是字节码行号,后者是 java 源码行号:start_pc:00 00,end_pc:00 03
上述属性表集合是位于方法表中,同样,inc()方法的字节码描述和上述方法描述基本一致,在方法表集合后面,会有一个位于Class文件的属性表集合,定义Class文件的一些属性表
- 本例中class文件的属性表字节码如下:
- attribute_count=0x0001,说明本Class文件只有一个属性表
- attribute_name_index=0x000D,说明该属性表索引指向第13个常量,即SourceFile,用于记录生成这个Class文件的源码文件名称,该属性表结构如下:
- attribute_length=0x00000002,说明属性长度为2字节
- sourcefile_index=0x000E,说明索引指向第14个常量,即aaa.java