文章目录
- 1. 简介
- 2. Class文件分析
1. 简介
Java有一个著名的口号一次编译,处处运行
,这就凸显出来Java程序的一个特点平台无关性
。Java的平台无关性是基于各种不同平台的Java虚拟机,以及所有平台都统一支持的程序存储格式—字节码实现的。在Java中任何一个Class文件都对应一个类或接口的定义信息。Class文件是以一组8个字节为单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件中,中间没有添加任何分隔符,当遇到需要占用8个字节以上空间的数据项时,会按照高位在前的方式分割成若干个8个字节进行存储。
2. Class文件分析
首先我们将下面代码编译成字节码:
public class testJVM {
private int m;
public int inc(){
return m+1;
}
}
生成的字节码文件如下:
- 魔数(Magic)
每个Class文件的头4个字节被称为魔数,它唯一的作用是确定这个文件是否为一个能被虚拟机接受Class文件(即判断Class文件是否符合JVM标准),Java的Class文件的模式数的默认值是0xCAFEBABE
。
- 版本号(Minor_version)
魔术的后面四个字节是Class文件的版本号,第5个和第6个字节是次版本号(Minor Version),第7第八个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号加1。
可以发现次版本号为0000
,主版本号为0034
,主版本号转化为10进制为52,也就是说明这个版本是可以被JDK 8或以上版本的虚拟机执行Class文件的(jdk(1+52-45))。
- 常量池(constant_pool_count)
在版本号后面就是常量池入口,常量池是Class文件的资源仓库,它是Class文件结构中与其它项目关联最多的数据,通常也是Class文件空间中最大的数据项之一。由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一个u2(2个字节的无符号整数)来代表常量池的容量。
可以看出常量池的大小为0x0016
,即十进制22,代表常量池中有21项常量,索引范围是1~21。思考:为什么不从1开始计数,其实Class文件中的其他集合类型,包括接口索引集合、字段表集合、方法表集合等都是从0开始的?
,Class文件这么设计是有目的,设计者将第0项常量空出来是为了如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池的项目”的含义,可以把索引值设置为0来表示。
常量池中主要存放两大类常量:字面量和符号引用,字面量比较接近Java语言层面的常量概念,如文本字符串、被声明的final的常量值等。而符号引用属于编译原理的概念,主要有以下常量:
- 被模块导出或者开放的包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
我们直接分析class文件好像不太好理解,我们可以使用JDK中的javap工具来数据class文件的字节码内容。
javap -verbose /Users/jackchai/Desktop/Self-study-notes/算法与数据结构/手写算法/jackchai/target/classes/com/jack/subject/util/testJVM.class
可以发现常量池有21个常量,和我们前面分析的一样,上面的内容在分析字节码的时候是很重要的,Java代码在进行Javac编译的时候,并不像C和C++那样有连接这一步骤,而是在虚拟机加载Class文件的时候进行动态连接,也就是说,在Class文件中不会保持各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期进行转换的话是无法得到真正的内存入口地址的,也就无法直接被虚拟机使用的。当虚拟机进行类加载的时候,将会从常量池中获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
常量池中的每一项常量都是一个表,最初常量表中共有11种结构各不相同的表结构数据,后来为例更好的支持动态语言调用,额外增加了4种动态语言相关的常量。为了更好的支持模块化系统,又加入了CONSTANT_Module_info
和CONSTANT_Package_info
两个常量,所以截止到JDK 13,常量表中分别有17中不同类型的常量。
回到我们的案例中,第一个常量为0x0a
,查表的10行,发现这是个类中方法的符号引用(CONSTANT_Methodref_info
)。我们查看Javap的结构,发现第一个常量的引用为java/lang/Object."<init>":()V
,这个是编译器自动生成的方法。从后面的表我们可以看出这种类型的常量需要占用5个字节。所以第一个常量在class文件中的位置如下:
然后第二个常量的位置为0x09
,为Const_Fieldref_info
,然后才Class文件中的位置如下:
后面的以此类推,就可以得出整个常量池的情况,如下图所示:
可以发现常量池在Class文件中占的比重还是比较大的。
常量池中的17种数据类型的结构总表
- 访问标志
在常量池结束之后,紧接着是2个字节代表访问标志(access_flag),这个标志用来标志由于你识别一些类或者接口的访问信息,包括这个Class是类还是接口,是否定义为public类型;是否被定义为abstract类型;如果是类的话,是否被声明为final等等。
access_flag一共有16个标志位可以使用,目前只使用了9种,没有使用到的一律为0。我们看一下我们案例的访问标志位:
为0x21
,即0x001|0x20
,所以查表得我们的类的ACC_PUBLIC
和ACC_SUPER
标志位为真。
- 类索引、父类索引与接口索引集合
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。类索引确定这个类的全限定名,父类索引确定这个类的父类的全限定名,由于Java语言不允许多继承所以父类索引只有一个,除了Java.lang.Object之外,所以的类都父类,因此出类Object外所有Java类的父类索引都不为0。接口索引集合描述这个类实现的接口,这些被实现的接口按照implents顺序排列在接口索引集合中。
我们回到我们的案例,这部分的字节码为:
首先是类索引为0x0003
,到我们常量池中找,可以找到这个索引指向的常量为com/jack/subject/util/testJVM
。然后父类索引为0x0004
查找常量池为java/lang/Object
,所以父类为Object类为。最后由于我们的测试类没有实现任何接口,所以接口索引集合为0x00
(前面说过设计者将第0项的常量空出来是为了表示不引向任何一个常量池项目的含义)。
- 字段表集合
字段表集合用于描述接口或者类中声明的变量。Java语言中的字段包括类级的变量和实例级变量,但不包括方法内部声明的局部变量。字段表的结构大体如下所示:
首先是access_flags,这个和前面类的访问标志的结构是很类似的,都是一个u2的数据类型。
访问标志后面就是两项索引值,name_index和descriptor_index。它们都是对常量池项的引用,分别代表着字段的简单名称以及字段和方法的描述符。这里我们比较一下“简单名称”“描述符”和“全限定名”三个字符串的概念。全限定名和简单名称很好理解,如前面的 com/jack/subject/util/testJVM
,就是把类全名的.
替换成了/
而已,为了使连续多个全限定名之间不产生混淆,在最后一般会加入;
符号表示一个全限定名的结束。简单名称就是指没有类型和参数修饰的方法和字段的名称,如这个类中的inc方法和m字段的简单名称就是inc
和m
。相比于全限定名和简单名称,方法和字段的描述符要多一些,描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符的规则,基本数据类型以及代表有无返回值的void类型第一个字母都要大些,而对象类型要用字符L加对象的全限定名来表示。
对于数组类型,每一个维度将使用一个前置的"["字符来表示,如java.lang.String[][]
类型会被记录为[[Ljava/lang/String
。用描述符来描述方法时,按照先参数列表、后返回值的顺序,如()V
。回到我们的案例:
首先是0x0001
说明这个类只有一个字段表数据。接下来是0x0002
代表为ACC_PRIVATE
,然后name_index
就是0x0005
,我到常量池中可以看到为m
,然后descriptor_index
的值为0x0006
,回到常量池中可以看到为I
即为int类型。到此字段表包含的固定数据项目到此结束,不过descriptor_index后面之后会跟随一个属性表集合,用来存储一些额外的信息。对于本例中它的属性计数器为0,也就是没有额外信息,如果m的声明改为final static int m=123
,那就可能存在一项名称为Constant_value
的属性,其值指向123。这里就不详细分析这一部分了。
字段表集合汇总不会列出从父类或者父类接口中基础而来的字段,但是有可能出现原本Java代码之外不存在的字段,譬如在内部类为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字段,另外在Java中字段是无法重载的,两个字段的数据类型不管是否相同,都必须使用不一样的名称,但是对于Class文件来讲,只要两个字段的描述符不是完全相同,那字段重名是合法的。
- 方法表集合
Class文件对于方法的描述与对字段的描述集合采用了完全一致的方法。
它的访问标志有下面几种:
回到我们的案例:
第一个u2类型的数据为0x00002
,代表集合中有两个方法,首先第一个方法的访问标志值为0x0001
,也就是ACC_PUBLIC
,name_index
为0x0007
,我们到常量池中一看是<init>
,descriptor_index
为0x0008
表示()V
。然后是属性计数器为0x0001
表示这个方法属性集合中有一项属性,属性名称为0x0009
,为Code
,说明此属性是方法的字节码描述。
与字段表集合对应地,如果父类方法在子类方法中没有重写,方法表集合中就不会出现来自父类的方法信息,但同样的会出现由编译器自动添加的方法,最常见的便是类构造器
<clinit>()
方法和实例构造器<init>()
方法。在Java语言中要重载一个方法,除了要与原方法具有相同的简单名称之外,还必须要有一个与原方法不同的特征签名。特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合,也正是因为返回值不会包含在特征签名中,所以Java语言是无法仅仅依靠返回值来对一个已有的方法进行重载的。但是Class文件中可以使用返回值区分。
- 属性表集合
属性表大家也可以看到前面已经出现过很多次了,Class文件、字段表和方法表都有自己的属性表集合,以描述某些特有场景。
属性表的结构如下:
我们挑一些重点属性来分析:
1. Code属性
Java程序的方法体里面的代码经过Javac编译器处理后,最终变为字节码指令存储载Code属性中。Code属性出现在方法表的属性集合中,但并非所有的方法表都必须存砸这个属性,例如接口就没有Code属性。
- attribute_name_index:指向
Constant_Utf8_info
型常量的索引,代表属性的属性名称,固定为Code(2个字节)- attribute_length:表示属性值的长度(4个字节)
- max_stack:代表操作数栈的深度最大值,JVM根据这个值来分配栈帧
- max_locals:代表局部变量表所需的存储空间,单位是变量槽(变量槽是JVM为局部变量分配内存最小单位),32位的数据类型如byte占用一个变量槽,64位如double和long占用两个变量槽。显示异常处理程序的参数(try-catch),方法参数(包括实例方法中的隐藏参数this),方法体定义的局部变量都是用局部变量表存储的,局部变量表占用的变量槽的数量就是
max_locals
的值。操作数栈和局部变量表直接决定了一个栈帧占用的内存。注意Java虚拟机会对变量槽进行重用,例如代码执行超过了一个局部变量表的作用域时,这个剧本变量的变量槽就可以被重用,JVM会根据同时生存的最大局部变量数量和类型计算max_locals
大小- code_length和code:用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code就是真正的字节码类型。COde属性是Class文件中最重要的一个属性,如果把一个Java代码中的信息分为代码和元数据类型,那么Code就是描述代码,其他数据项目用于描述元数据。
回到案例,我们继续向后分析Class文件:
首先操作数栈的最大深度和本地变量表的容量都是0x0001
,字节码长度为0x00000005
,虚拟机读区到的字节码就是后面的5个字节0x2ab70001b1
。加入JVM需要执行这个方法,那么过程描述如下:
- 读入2a:
0x2a
对应的指令为aload_0,这个指令的含义是将第0个变量槽中为reference类型的本地变量推松到操作数栈的栈顶 - 读入b7:对应指令为invokespecial,这条指令的作用是以栈顶的refrence类型的数据所指向的对象为方法接收者,调用此对象的实例构造方法,private方法或者它父类的方法。这个方法有一个u2类型的参数说明具体调用哪个方法,它指向常量池的一个
CONSTANT_Methodref_info
类型常量,即此方法的符号引用 - 读入0001:这个invokespecial指令的参数,代表一个符号引用,可以从常量池中可以看出是
java/lang/Object."<init>":()V
即构造方法的符号引用。 - 读入B1:代表return指令,表示方法的返回
我们看Javap指令对字节码的计算结果,可以看出这个方法的大体结构:
注意不管实例方法有没有参数args_size
都是从1开始算的,这是因为在任何实例方法中,都可以通过this访问到此方法所属的对象。因此在实例方法中的局部变量表中至少会存在一个指向当前对象实例的局部变量表。现在我们分析一下如果方法存在异常,那么方法的字节码结构又会变成什么样子,我们修改一下inc方法代码;
public class testJVM {
private int m;
public int inc(){
int x;
try{
x=1;
return x;
}catch (Exception a){
x=2;
return x;
}finally {
x=3;
}
}
}
现在inc方法的字节码计算出来就是下面这种情况:
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: istore_2
4: iconst_3
5: istore_1
6: iload_2
7: ireturn
8: astore_2
9: iconst_2
10: istore_1
11: iload_1
12: istore_3
13: iconst_3
14: istore_1
15: iload_3
16: ireturn
17: astore 4
19: iconst_3
20: istore_1
21: aload 4
23: athrow
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
17 19 17 any
LineNumberTable:
line 10: 0
line 11: 2
line 16: 4
line 11: 6
line 12: 8
line 13: 9
line 14: 11
line 16: 13
line 14: 15
line 16: 17
line 17: 21
LocalVariableTable:
Start Length Slot Name Signature
2 6 1 x I
9 8 2 a Ljava/lang/Exception;
11 6 1 x I
0 24 0 this Lcom/jack/subject/util/testJVM;
21 3 1 x I
StackMapTable: number_of_entries = 2
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
}
可以发现多了一个Exception table即异常表,异常表的结构如下所示:
如果当字节码从start_pc行到end_pc行之间(不包含end_pc行)出现了来信为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到handler_pc行继续处理,当catch_type为0时,代表任意异常都需要转到handler_pc进行处理。
2. Exceptions属性
这里的Exceptions属性是在方法中与Code平级的属性,不要和前面的异常产生混淆,Exceptions属性的作用时列举出方法中可能抛出的受检查异常(Checked Exceptions),也就是方法描述时在throws关键字后面的异常。它的结构如下:
3. LineNumberTable属性
用于描述Java源码行与字节码行号(字节码偏移量)之间的对应关系(断点机制和这个有关,有利于程序调试)。它的结构如下:
4. LocalVariableTable和LocalVariableTypeTable属性
前者用于描述栈帧中局部变量表与Java源码中定义的变量的关系,如果没有设置这个属性,别人引用这个方法时,所有参数名会丢失。其结构如下:
其中local_variable_info就是代表了一个栈帧与源码中局部变量之间的关联关系,它也有自己的结构:
- start_pc和length属性:代表这个局部变量的生命周期的字节码偏移量和作用的范围,通过这两个值JVM可以知道局部变量在字节码中的作用域
- name_index和descriptor:指向常量池,代表该局部变量的名称以及这个局部变量的描述符
- index:就是这个局部变量在栈帧中的变量槽的位置
在JDK 5引入泛型后,LocalVariableTable属性新增量一个姐妹属性“LocalVariableTypeTable”这个新增的属性结于LocalVariableTable非常类似,仅仅是把记录字段描述符的descriptor_index替换成了字段的特征签名,对于非泛型来说,描述符和特征签名能描述的信息是能吻合的,但是泛型引入后,由于描述符的参数化类型被擦除,描述符就不能准确描述访型信息了,所以出现了这个属性,使用字段的特征签名来完成泛型的描述。
5. SourceFile属性
SourceFile用来记录生成这个class文件的源码文件。
sourcefile_index数据项是指向常量池中的CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名。
6. ConstantValue
该属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量才能使用这个属性,对于非static类型的变量的赋值是在<init>()方法中进行的,对于类变量,则有两种选择,首先是在类构造器<clinit>()方法中或者使用ConstantValue属性。如果同时使用final和static修饰一个变量,并且这个变量类型是基本类型或者java.lang.String类型就会使用ConstantValue进行初始化。否则用<clinit>()初始化。
7. InnerClasses属性
用于记录内部类和宿主类之间的关联。如果一个类中定义了内部类,那么编译器就会为它及内部类生成InnerClasses属性。InnerClasses属性结构如下:
number_of_classes
代表该类的内部类数量,inner_classes_info
用于描述内部类的信息,它也有自己的结构
- inner_class_info_index:表示指向常量池中CONSTATN_Class_info型变量的索引,代表内部类的引用(外部类对内部类的引用)
- inner_class_info_index:内部类对外部类的引用
- inner_class_access_flag:内部类的访问标志
- inner_name_index:指向常量池中
CONSTANT_Utf8_info
型常量的索引,代表这个内部类的名称,匿名内部类,这项值为0
8. Deprecated及Synthetic属性
Deprecated和Synthetic这两个属性都是布尔属性,前者表示某个类、字段或者方法,是否已经被作者指定为不再推荐使用,使用@deprecated
注解标记。后者表示这个方法并不是由Java代码产生,而是由编译器自动添加的,在JDK5之后,标识一个类,字段或者方法是编译器自动产生的,也可以设置访问标记中的ACC_SYNTHETIC
标志。编译器通过生成一些在源代码中不存在的synthetic方法,字段或者整个类的方式,实现了越权访问(越过private修饰器)或者其他语法限制功能,例如枚举类中自动生成的枚举元素数组和嵌套类的桥接方法。
9. StackMapTable属性
是JDK 6增加到Class文件规范中的,它是一个相当复杂的变长属性,位于Code属性表中。这个属性会在虚拟机加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。StackMapTable属性中包含0到多个栈映射帧,每个栈映射帧都显式或者隐式地代表一个字节码偏移量,用于表示执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型确定一段字节码指令是否符合逻辑约束。
10.Signature属性
是JDK 5增加到Class规范中的,它是一个可选的定长属性,可以出现在类、字段表和方法表结构的属性表中。在JDK5中大幅增强类Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含类型变量或参数化类型,则Signature属性会为它记录泛型签名信息。之所以要专门设置这一属性取记录泛型类型,是因为Java语言的泛型是采用的擦除法实现的伪泛型,字节码(Code)中所有的泛型信息编译(类型变量、参数化类型)在编译期都会擦除。这种方式就是实现简单,但是缺点也很明显,就是无法在运行期像C#一样真泛型支持的语言那样,将泛型类型当成用户定义的普通类型一样看待,例如运行期的反射时无法获取泛型信息,Signature属性的出现就是为了解决这个问题,限制Java的反射API能够获取泛型类型,最终的类型也是来源于这个属性。
其中signature_index项是一个对常池的有效索引,常量池在该索引处必须是CONSTANT_Utf8_info结构,表示类签名或方法签名或字段签名。如果当前的Signature属性是类文件属性,则这个结构表示类签名,如果是方法表属性就是方法类型签名,如果是字段表属性,就是字段类型签名。
11. BootstrapMethods属性
在JDK 17增加到Class文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。如果某个类文件结构的常量池中曾经出现过 CONSTANT_InvokeDynamic_info
类型的常量,那么这个类文件的属性表中必须存在一个明确的BootstrapMethods属性,另外,即使CONSTANT_InvokeDynamic_info
类型的常量在常量池中出现多次,类文件的属性表中最多也只能有一个BootstrapMethos属性。BootstrapMethos属性和JSR-292中的InvokeDynamic指令和java.lang.Invoke包关系非常密切,要介绍这个属性的功能我们首先需要知道InvokeDynamic指令的运作原理。虽然JDK 7中已经提供类InvokeDynamic指令,但这个版本的Javac编译器还暂时无法支持InvokeDynamic指令和生成BootstrapMethods属性,必须通过一些非常规的手段才能使用它们,知道JDK8中Lambda表达式和接口默认方法的出现,InvokeDynamic指令才算在Java语言生成的Class文件中有了用武之地。
其中引用到了boostrap_method结构如下所示:
BootstrapMethods属性里,num_boostrap_methods项的值给出来boostrap_methods[]数组中的引导方法限定符的数量。而bootstrap_methods[]数组的每个成员包含一个指向常量池CONSTANT_MethodHandle结构的索引值,它代表一个引导方法。还包含了这个引导方法静态参数的序列(可能为空)。boostrap_methods[]数组的每个成员必须包含下三项内容:
- boostrap_method_ref:必须是一个对常量池的有效索引。常量池在该索引处的值是一个CONSTANT_MethodHandle_info结构
- num_boostrap_arguments:num_boostrap_arguments项的值给出了boostrap_arguments[]数组成员数量
- boostrap_arguments[]: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
12. MethodParameter属性
MethodParameters是JDK 8时新加入到Class文件格式中,它是一个用在方法表中的变长属性。MethodParameters的作用是记录方法的各个参数名称和信息。它是方法表属性,与Code属性平级的,可以运行时反射API获取。
其中,name_index时一个指向常量池CONSTANT_Utf8_info常量的索引值,代表了参数的名称。而access_flagsf时参数的状态指示器,它可以包含以下三种状态中的一种或多种:
- 0x0010(ACC_FINAL):表示该参数被final修饰
- 0x1000(ACC_SYNTHETIC):表示该参数并未出现在源文件中,是编译器自动生成的
- 0x8000(ACC_MANDATED):表示该参数是在源文件中隐式定义的,Java语言中典型的场景是this关键字
13. 运行时注解相关属性
早在JDK 5时期,Java语言的语法进行了多项增强,其中之一就是提供了对注解的支持。为例存储源码中的注解信息,Class文件同步增加了RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations和RuntimeInvisibleParameterAnnotations四个属性。到了JDK 8时期,进一步加强了Java语言的注解使用范围,又增类型注解,所以Class文件中同步增加了RuntimeVisibleTypeAnnotations和RuntimeInvisibleTypeAnnotations两个属性,由于这六个属性不论结构还是功能都必须雷同,因此我们将它们合并在一起,以RuntimeVisibleAnnotations为代表进行介绍。RuntimeVisibleAnnotations是一个变长属性,它记录类、字段和方法的上面上记录运行时可见的注解,当我们使用反射API来获取类、字段或方法上的注解时,返回值就是通过这个属性来取到的。RuntimeVisibleAnnotations属性的结构如下:
type_index是一个指向常量池CONSTANT_Utf8_info常量索引值,该常量描述符的形式表示一个注解,num_element_value_pairs时element_value_pairs数组的计数器,elements_value_pairs中每个元素都是一个键值对,代表该注解的参数和值。