第6章 类文件结构

Class类文件的结构


魔数与Class文件的版本


每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用

魔数来进行身份识别,譬如图片格式,如gif或者jpeg等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展

名可以随意地改动。文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。Class文件的魔数的获得很有

“浪漫气息”,值为:0xCAFEBABE(咖啡宝贝?),这个魔数值在Java还称做“Oak”语言的时候就已经确定下来了。

紧接着魔数的4个字节存储的是Class文件的版本号:第5个和第6个是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本

号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,

即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。


常量池


紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class

文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。

由于常量池中常量的数值是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final

的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:

类和接口的全限定名(Fully Qualified Name)

字段的名称和描述符(Descriptor)

方法的名称和描述符

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会

保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。

当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。


访问标志


在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;

是否定义为public类型;是否定义为abstract类型:如果是类的话,是否被声明为final等。


类索引、父类索引与接口索引集合


类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中

由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,

所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合

就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到

右排列在接口索引集合中。

类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为

CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限

定名字符串。


字段表集合


字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类集变量以及实例级变量,但不包括在方法内部声明的局部变量。我们可以想一想

在Java中描述一个字段可以包含什么信息?可以包括的信息有:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、

可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、

字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示,而字段叫什么名字、字段被定义为什么数据

类型,这些都是无法固定的,只能引用常量池中的常量来描述。


方法表集合


Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、

名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。



属性表集合


与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有

属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。


1.Code属性


Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法都

必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。


2.Exceptions属性


Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Exceptions),也就是方法描述时在throws关键字后面列举的异常。


3.LineNumberTable属性


LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件

之中,可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响

就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。


4.LocalVariableTable属性


LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件

之中,可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有

的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且

在调试期间无法根据参数名称从上下文中获得参数值。


5.SourceFile属性


SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以分别使用Javac的-g:none或-g:source选项来关闭或要求生成这项

信息。在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中

将不会显示出错代码所属的文件名。


6.ConstantValue属性


ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。


7.InnerClasses属性


InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。


8.Deprecated及Synthetic属性


Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。

Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,他可以通过在代码中使用@deprecated注释进行设置。

Synthetic属性代表此字段或者方法并不是有Java源码直接产生的,而是由编译器自行添加的,在JDK 1.5之后,标识一个类、字段或者方法是编译器自动产生

的,也可以设置它们访问标志中的ACC_SYNTHETIC标志位,其中最典型的例子就是Bridge Method。所有由非用户代码产生的类、方法及字段都应当至少

设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器“<init>”方法和类构造器“<clinit>”方法。


9.StackMapTable属性


StackMapTable属性在JDK 1.6发布后增加到了Class文件规范中,它是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的

字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。


10.Signature属性


Signature属性在JDK 1.5发布后增加到了Class文件规范之中,它是一个可选的定长属性,可以出现于类、属性表和方法表结构的属性表中。在JDK 1.5中

大幅增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型

(Parameterized  Types),则Signature属性会为它记录泛型签名信息。


11.BootstrapMethods属性


BootstrapMethods属性在JDK 1.7发布后增加到了Class文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存

invokedynamic指令引用的引导方法限定符。


字节码指令简介


加载和存储指令


加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容。

将一个局部变量加载到操作栈

将一个数值从操作数栈存储到局部变量表

将一个常量加载到操作数栈

扩充局部变量表的访问索引的指令


运算指令


加法指令;减法指令;乘法指令;触发指令;求余指令;取反指令;位移指令;按位或指令;按位与指令;按位异或指令;

局部变量自增指令;比较指令


类型转换指令


类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作,或者用来处理

本节开篇所提到的字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

Java虚拟机直接支持(即转换时无需显式的转换指令)以下数值类型的宽化类型转换(Widening  Numeric Conversions,即小范围类型向大范围类型

类型的安全转换):

int——>long,float,double

long——>float,double

float——>double


在将一个浮点值窄化转换为整数类型T(T限于int或long类型之一)的时候,将遵循以下转换规则:

如果浮点值是NaN,那转换结果就是int或long类型的0

如果浮点值不是无穷大的话,浮点值使用IEEE 754的向零舍入模式取整,获得整数值v,如果v在目标类型T(int或long)的表示范围之内,那转换结果就是v。

否则,将根据v的符号,转换T所能表示的最大或者最小正数。


对象创建与访问指令


虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。对象创建后,就可以通过对象访问指令获取对象实例

或者数组实例中的字段或者数组元素,这些指令如下。

创建类实例的指令

创建数组的指令

访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令

把一个数组元素加载到操作数栈的指令

将一个操作数栈的值存储到数组元素中的指令

取数组长度的指令

检查类实例类型的指令


操作数栈管理指令


如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:

将操作数栈的栈顶一个或两个元素出栈

复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶

将栈最顶端的两个数值呼唤


控制转移指令


条件分支;复合条件分支;无条件分支


方法调用和返回指令


invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。

invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。

invokestatic指令用于调用类方法(static方法)

invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机

内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。


异常处理指令


同步指令


同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令

来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java编译器与Java虚拟机两者共同协作支持。



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值