一、字节码文件
1.1 字节码文件是什么
源代码经过编译器编译之后便会生成一个字节码文件(.class),字节码是一种二进制的类文件,它的内容是JVM的指令,而不像C、C++经由编译器直接生成机器码。Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码[+操作数]构成。
图1.1 JVM指令示例
1.2 字节码文件的跨平台性--【java-跨平台的语言,JVM-跨语言的平台】
Java虚拟机不和包括Java在内的任何语言绑定,它只与"Class文件"这种特定的二进制文件格式所关联。无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行,可以说,统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁。
所有的JVM全部遵守Java虚拟机规范,也就是说所有的JVM环境都是一样的(虽然在MAC/Win/Linux平台有各自的实现,但提供的JVM环境是一致的),这样一来字节码文件可以在各种JVM上进行(跨平台)。
1.3 前端编译器与后端编译器(JIT)
- 前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件。将Java源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法分析、语法分析、语义分析以及生成字节码。前端编译器并不会直接涉及编译优化等方面的技术(前端编译器也涉及一些代码优化,如:字符串字面量的拼接),而是将这些具体优化细节移交给HotSpot的JIT编译器负责。
- javac、内置在eclipse中的ECJ(Eclipse Compiler for Java)是常见的前端编译器
二、class文件结构
Class文件的总体结构如下:
- 魔数
- Class文件版本
- 常量池
- 访问标志
- 类索引、父类索引、接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
三、魔数
魔数值固定为0xCAFEBABE,不会改变。如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出错误。
四、Class文件版本
不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件(向下兼容),但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件(会抛出java.lang.UnsupportedClassVersionError异常)。
各编译器对应的主、副版本号如下图:
举例--jdk1.8编译形成的class文件
五、常量池
常量池是Class文件中内容最为丰富的区域之一,可以说常量池是整个Class文件的基石。在版本号之后,紧跟着的是常量池的数量、常量池表。
- 常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count)
- 常量池表是一种表结构,以1~constant_pool_count-1为索引,表明了后面有多少个常量项(即常量池表项)
- 常量池中每一项常量又都是一个表,这些表的共同点是:表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。
class文件中常量池举例:
5.1 常量池表项类型及对应表结构
JDK1.7之后共14种不同的表结构数据,如下表格所示:(class文件中常量池举例中的0A对应标志10;09对应标志9;0c对应标志12)
标志 | 常量类型 | 描述 | 表结构细节 | 长度 | 细节描述 |
1 | CONSTANT_utf8_info | UTF-8编码的字符串(文本字符串) | tag | u1 | 值为1 |
length | u2 | UTF-8编码的字符串占用的字符数 | |||
bytes | u1 | 长度为length的UTF-8编码的字符串 | |||
3 | CONSTANT_Integer_info | 整型字面量 | tag | u1 | 值为3 |
bytes | u4 | 按照高位在前存储的int值 | |||
4 | CONSTANT_Float_info | 浮点型字面量 | tag | u1 | 值为4 |
bytes | u4 | 按照高位在前存储的float值 | |||
5 | CONSTANT_Long_info | 长整型字面量 | tag | u1 | 值为5 |
bytes | u8 | 按照高位在前存储的long值 | |||
6 | CONSTANT_Double_info | 双精度浮点型字面量 | tag | u1 | 值为6 |
bytes | u8 | 按照高位在前存储的double值 | |||
7 | CONSTANT_Class_info | 类或接口的符号引用 | tag | u1 | 值为7 |
index | u2 | 指向全限定名常量项的索引 | |||
8 | CONSTANT_String_info | 字符串类型字面量符号引用 | tag | u1 | 值为8 |
index | u2 | 指向字符串字面量的索引 | |||
9 | CONSTANT_Fieldref_info | 字段的符号引用 | tag | u1 | 值为9 |
index | u2 | 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项 | |||
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引项 | |||
10 | CONSTANT_Methodref_info | 类中方法的符号引用 | tag | u1 | 值为10 |
index | u2 | 指向声明方法的类描述符CONSTANT_Class_Info的索引项 | |||
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引项 | |||
11 | CONSTANT_InterfaceMethodref_info | 接口中方法的符号引用 | tag | u1 | 值为11 |
index | u2 | 指向声明方法的接口描述符CONSTANT_Class_Info的索引项 | |||
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引项 | |||
12 | CONSTANT_NameAndType_info | 字段或方法的符号引用 | tag | u1 | 值为12 |
index | u2 | 指向该字段或方法名称常量项的索引 | |||
index | u2 | 指向该字段或方法描述符常量项的索引 | |||
15 | CONSTANT_MethodHandle_info | 表示方法句柄 | tag | u1 | 值为15 |
reference_kind | u1 | 值必须在1-9之间,它决定了方法句柄的类型方法句柄类型的值表示方法句柄的字节码行为 | |||
reference_index | u2 | 值必须是对常量池的有效索引 | |||
16 | CONSTANT_MethodType_info | 标志方法类型 | tag | u1 | 值为16 |
descriptor_index | u2 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符 | |||
18 | CONSTANT_InvokeDynamic_info | 表示一个动态方法调用点 | tag | u1 | 值为18 |
bootstrap_method_attr | u2 | 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引 | |||
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_Info结构,表示方法名和方法描述符 |
- 这14种表的共同点是:表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型
- 在常量池列表中,CONSTANT_Utf8_info常量项是一种使用改进过的UTF-8编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息
- 这14种常量项结构还有一个特点是,其中13个常量项占用的字节固定,只有CONSTANT_Utf8_info占用字节不固定,其大小由length决定。为什么?
因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串(基本数据类型除外),这些字符串的大小是在编写程序时才确定。比如你定义一个类,类名可以取长去短,所以在没编译前,大小不固定,编译后,通过UTF-8编码,就可以知道其长度。
- 在Class文件的常量池中,所有的8字节常量均占两个表项的空间。即:如果一个CONSTANT_Long_info或CONSTANT_Double_info结构的项在常量池表中的索引位n,则常量池表中下一个可用项的索引位n+2,此时常量池表中索引为n+1的项仍然有效但必须视为不可用的
常量池中为什么包含这些内容?
Java代码在进行javac编译的时候,并不像C和C++那样有"连接"这一步骤,而是在虚拟机加载Class文件的时候进行动态链接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
【常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处)】
5.2 常量池表项存什么?--字面量和符号引用
常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放(jdk1.8之后,字符串常量池放在堆中)
常量池表中存放的是字面量和符号引用,最终这些内容都会是一个文本字符串(final修饰的基本数据类型常量除外)
常量池表项 | 具体常量 | 举例 |
字面量 | 文本字符串 (对应CONSTANT_utf8_info类型) | private int num1=10语句中的num1 |
String str=“abc”语句中的abc和str | ||
String str=new String("cde")语句中的cde和str | ||
final修饰的常量 | private final int num=10语句中的num和10 | |
符号引用 | 类和接口的全限定名 | 如com/fuping3/Person(仅仅是把包的"."替换成"/",为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个";"表示全限定名结束) |
属性名称和描述符 | 描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值 | |
方法名称和描述符 |
5.2.1 描述符
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名表示,详见下表:
描述符举例:
如方法java.lang.String toString()的描述符为()Ljava/lang/String;,方法int abc(int[]x,inty)描述符为([II)I
5.2.2 符号引用
Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从class文件常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用(翻译到具体的内存地址中)。
符号引用和直接引用的区别:
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
5.2.3 举例
源码
final修饰的引用类型常量,不在class文件常量池中;class文件在加载以后,final修饰的引用类型常量不在运行时常量池,而是在堆中
import com.fupng3.pojo.Man;
public class MyString2 {
private String str="abc";//常量为CONSTANT_String_info类型的abc,和CONSTANT_utf8_info类型的str
private final String str2="cde";//常量为CONSTANT_String_info类型的cde,和CONSTANT_utf8_info类型的str2
private int num1=10;//常量为CONSTANT_utf8_info类型的num,注意常量池中没有10
private final int num2=20;//常量为CONSTANT_utf8_info类型的num2和20
private final double num3=30;//常量为CONSTANT_utf8_info类型的num3和30.0,占2个表项空间
private Dog dog=new Dog(20);//常量为CONSTANT_utf8_info类型的dog
private final Man man=new Man();//常量为CONSTANT_utf8_info类型的man,注意,class文件常量池中并无引用类型常量
}
执行javap -v MyString2.class命令查看二进制的class文件中的常量池
举例中class文件中常量池全部内容
constant pool:
#1 = Methodref #18.#42 // java/lang/Object."<init>":()V
#2 = String #43 // abc
#3 = Fieldref #17.#44 // com/fupng3/str/MyString2.str:Ljava/lang/Strin
#4 = String #45 // cde
#5 = Fieldref #17.#46 // com/fupng3/str/MyString2.str2:Ljava/lang/Stri
#6 = Fieldref #17.#47 // com/fupng3/str/MyString2.num1:I
#7 = Fieldref #17.#48 // com/fupng3/str/MyString2.num2:I
#8 = Double 30.0d
#10 = Fieldref #17.#49 // com/fupng3/str/MyString2.num3:D
#11 = Class #50 // com/fupng3/pojo/Dog
#12 = Methodref #11.#51 // com/fupng3/pojo/Dog."<init>":(I)V
#13 = Fieldref #17.#52 // com/fupng3/str/MyString2.dog:Lcom/fupng3/pojo
#14 = Class #53 // com/fupng3/pojo/Man
#15 = Methodref #14.#42 // com/fupng3/pojo/Man."<init>":()V
#16 = Fieldref #17.#54 // com/fupng3/str/MyString2.man:Lcom/fupng3/pojo
#17 = Class #55 // com/fupng3/str/MyString2
#18 = Class #56 // java/lang/Object
#19 = Utf8 str
#20 = Utf8 Ljava/lang/String;
#21 = Utf8 str2
#22 = Utf8 ConstantValue
#23 = Utf8 num1
#24 = Utf8 I
#25 = Utf8 num2
#26 = Integer 20
#27 = Utf8 num3
#28 = Utf8 D
#29 = Utf8 dog
#30 = Utf8 Lcom/fupng3/pojo/Dog;
#31 = Utf8 man
#32 = Utf8 Lcom/fupng3/pojo/Man;
#33 = Utf8 <init>
#34 = Utf8 ()V
#35 = Utf8 Code
#36 = Utf8 LineNumberTable
#37 = Utf8 LocalVariableTable
#38 = Utf8 this
#39 = Utf8 Lcom/fupng3/str/MyString2;
#40 = Utf8 SourceFile
#41 = Utf8 MyString2.java
#42 = NameAndType #33:#34 // "<init>":()V
#43 = Utf8 abc
#44 = NameAndType #19:#20 // str:Ljava/lang/String;
#45 = Utf8 cde
#46 = NameAndType #21:#20 // str2:Ljava/lang/String;
#47 = NameAndType #23:#24 // num1:I
#48 = NameAndType #25:#24 // num2:I
#49 = NameAndType #27:#28 // num3:D
#50 = Utf8 com/fupng3/pojo/Dog
#51 = NameAndType #33:#57 // "<init>":(I)V
#52 = NameAndType #29:#30 // dog:Lcom/fupng3/pojo/Dog;
#53 = Utf8 com/fupng3/pojo/Man
#54 = NameAndType #31:#32 // man:Lcom/fupng3/pojo/Man;
#55 = Utf8 com/fupng3/str/MyString2
#56 = Utf8 java/lang/Object
#57 = Utf8 (I)V
六、访问标记
在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型(外部类只能用public或缺省修饰符修饰);是否定义为abstract类型;如果是类的话,是否被声明为final等。各种访问标记如下所示:
七、class文件中其他结构--暂略