JVM--02--class文件

一、字节码文件

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文件的总体结构如下:

  1. 魔数
  2. Class文件版本
  3. 常量池
  4. 访问标志
  5. 类索引、父类索引、接口索引集合
  6. 字段表集合
  7. 方法表集合
  8. 属性表集合

 

三、魔数


魔数值固定为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)

标志常量类型描述表结构细节长度细节描述
1CONSTANT_utf8_infoUTF-8编码的字符串(文本字符串)tagu1值为1
lengthu2UTF-8编码的字符串占用的字符数
bytesu1长度为length的UTF-8编码的字符串
3CONSTANT_Integer_info整型字面量tagu1值为3
bytesu4按照高位在前存储的int值
4CONSTANT_Float_info浮点型字面量tagu1值为4
bytesu4按照高位在前存储的float值
5CONSTANT_Long_info长整型字面量tagu1值为5
bytesu8按照高位在前存储的long值
6CONSTANT_Double_info双精度浮点型字面量tagu1值为6
bytesu8按照高位在前存储的double值
7CONSTANT_Class_info类或接口的符号引用tagu1值为7
indexu2指向全限定名常量项的索引
8CONSTANT_String_info字符串类型字面量符号引用tagu1值为8
indexu2指向字符串字面量的索引
9CONSTANT_Fieldref_info字段的符号引用tagu1值为9
indexu2指向声明字段的类或接口描述符CONSTANT_Class_info的索引项
indexu2指向字段描述符CONSTANT_NameAndType的索引项
10CONSTANT_Methodref_info类中方法的符号引用tagu1值为10
indexu2指向声明方法的类描述符CONSTANT_Class_Info的索引项
indexu2指向名称及类型描述符CONSTANT_NameAndType的索引项
11CONSTANT_InterfaceMethodref_info接口中方法的符号引用tagu1值为11
indexu2指向声明方法的接口描述符CONSTANT_Class_Info的索引项
indexu2指向名称及类型描述符CONSTANT_NameAndType的索引项
12CONSTANT_NameAndType_info字段或方法的符号引用tagu1值为12
indexu2指向该字段或方法名称常量项的索引
indexu2指向该字段或方法描述符常量项的索引
15CONSTANT_MethodHandle_info表示方法句柄tagu1值为15
reference_kindu1值必须在1-9之间,它决定了方法句柄的类型方法句柄类型的值表示方法句柄的字节码行为
reference_indexu2值必须是对常量池的有效索引
16CONSTANT_MethodType_info标志方法类型tagu1值为16
descriptor_indexu2值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符
18CONSTANT_InvokeDynamic_info表示一个动态方法调用点tagu1值为18
bootstrap_method_attru2值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_type_indexu2值必须是对当前常量池的有效索引,常量池在该索引处的项必须是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文件中其他结构--暂略

 

 

 

 

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值