类文件结构精华要点,面试必备

无关性的基石

各种不同平台平台的虚拟机与所有平台都统一使用的程序存储格式---字节码,是实现平台无关性的基石。然而,语言无关性也渐渐被开发者重视,实现语言无关性的基础仍然是虚拟机和字节码存储格式,Java虚拟机不和包括Java之内的所有语言邦定,它只与class文件,这种特定的二进制格式文件关联,class文件中包含了Java虚拟机指令集和符号表以及其他辅助信息。

                                                              Java虚拟机提供的语言无关性

类文件结构

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类和接口信息不定非得定义在文件里,譬如可以通过类加载器生成。Class文件是一组以8字节为基础单位的二进制流,每个数据项都按一定的规则紧密排列,中间没有任何分隔符。Class文件采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型,无符号数和表。

无符号数以u1,u2,u4,u8来代表1,2,4,8个字节,通常用来描述数字,索引,数量值,或以UTF8编码构成字符串值。

表结构是以无符号数或其他表作为数据项的复杂结构。

每一个Class文件开头的4字节称为魔法数,他的唯一作用是确定该文件是否是一个能被虚拟机接受的Class文件,如下图:

魔法数后面的4个字节是版本号信息,第五第六是次版本号信息,第七第八是主版本号信息,如下图,Java的版本号是从45开始的,对应的版本号是JDK1.0,0x34的十进制是52,对应的版本号为JDK1.8。

紧接着版本号之后的是常量池入口,常量池可以理解为Class文件的资源仓库,由于常量池中常量的数量不固定,所以在入口处放一个u2来记录常量池中的常量数量,例如1则代表常量池中有一个常量。如下图中00 35 代表有53个常量。常量池中主要存放两大类常量,字面量和符号引用。字面量比较符合Java语言中常量的概念,比如字符串,final修饰的数值等,符号引用只要包括三类常量:类和接口全限定名字段的名称和描述符方法的名称和描述符

常量池结束之后,紧接着的两个字节代表访问标志,这个标志用来识别类或者接口层次的访问信息。包括Class是类还是接口,是否定义为Public,是否定义为abstract等,具体的标志位以及标记的含义如下表:

类索引,父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和访问标志索引用两个U2类型的索引值表示,对于接口索引集合,入口处的U2类型的数据代表接口索引数据的数量。

紧接着后面的还有字段表,方法表和属性表。字段表用于描述接口或类中声明的变量,字段包括类级变量,实例级变量,但是不包括方法内的局部变量。方法表与字段表接口类似,依次包括访问标志,名称索引,描述符索引,属性集合几项,仅在访问标志和属性表集合的可选项上有所区别。Class文件,字段表,方法表都可以携带自己的属性表用来面熟专属场景的某些信息。

属性介绍

  1. Code属性Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。接口或者抽象类中的方法就不存在Code属性。
  2. Exceptions属性是在方法表中与Code属性平级的一项属性,读者不要与前面刚刚讲解完的异常表产生混淆。Exceptions属性的作用是列举出方法中可能抛出的受查异常(CheckedExcepitons),也就是方法描述时在throws关键字后面列举的异常。
  3. LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。
  4. LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。
  5. SourceFile属性用于记录生成这个Class文件的源码文件名称,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。
  6. ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。对于非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性。
  7. InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。
  8. Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过@deprecated注释进行设置。Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的。
  9. StackMapTable属性在JDK 1.6发布后增加到了Class文件规范中,它是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用(见7.3.2节),目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
  10. Signature属性在JDK 1.5发布后增加到了Class文件规范之中,它是一个可选的定长属性,可以出现于类、属性表和方法表结构的属性表中。在JDK 1.5中大幅增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。
  11. BootstrapMethods属性在JDK 1.7发布后增加到了Class文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。

字节码指令简介

上文中提到的类文件结构中对一个Class文件中的变量,方法以及类信息进行了描述,并未提到方法体的内容是如何定义在Class文件中的,其实方法体是被编译成字节码指令存储在了Code属性中。Java虚拟机的字节码指令由一个字节长度的,代表着某种操作含义的数字(操作码)以及虽有的零至多个此操作所需参数(操作数)组成,大多数指令都不包含操作数,只有一个操作码。

  1. 加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈(见第2章关于内存区域的介绍)之间来回传输。
  2. 运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
  3. 类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理本节开篇所提到的字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
  4. 对象创建与访问指令,虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令(在第7章会讲到数组和普通类的类型创建过程是不同的)。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素。
  5. 作数栈管理指令如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令。
  6. 控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。
  7. 方法调用和返回指令
  8. 异常处理指令在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,除了用throw语句显式抛出异常情况之外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。

总结

Java虚拟机必须实现能够读取Class文件并精确实现包含在其中的Java虚拟机语义,在满足Java虚拟机规范的前提下,对Java虚拟机的实现做出修改和优化。

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值