深入理解Java虚拟机(五)类文件结构

在java代码运行时,JVM真正运行的都是由.java文件编译生成的.class文件。任何一个.class文件,都对应着一个类或接口的定义信息。

class文件简单介绍:

    Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。

根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表

  • 无符号数:以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。
  • class文件主要由魔数、版本号、常量池、访问标志、类索引(还包括父类索引和接口索引集合)、字段表集合、方法表集合、字段表集合。

class文件详细介绍

魔数
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。Class文件的魔数的获得很有“浪漫气息”,值为:0xCAFEBABE(咖啡宝贝?)
版本号
紧接着魔数的4个字节是Class文件版本号。版本号又分为次版本号和主版本号。其中前两个字节用于表示次版本号,后两个字节用于表示主版本号。这个的版本号是与jdk版本相关的。
常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
  • 常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。
  • 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。符号引用则属于编译原理方面的概念,包括了下面三类常量:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。
  • Java代码在进行Javac编译时,是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
  • 常量池中每一项常量都是一个表,都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位,也就是下表中的tag值。不同的tag值代表了不同类型的常量。
  • 由下表可以知道,长度不确定的表是有一个length来描述该常量的二进制长度。
    在这里插入图片描述

以一个class文件的从头开始进行解析(以下提到的数都是16进制的,省略了前面的0x):
在这里插入图片描述

  1. CAFEBABE作为class文件的魔数。
  2. 0000代表class文件的次版本号。
  3. 0032代表主版本号。
  4. 0016是十进制的22,表示常量池中常量的个数,常量池的索引从1开始,所以索引为1-21。(之所以不用0作为索引,目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。)
  5. 07代表某个常量的tag值,此时去上面表中找tag值是7的常量类型,是CONSTANT_Class_info类型。CONSTANT_Class_info的常量对应的表除了tag值以外,还有个Index,这个index代表指向全限定名常量(就是类名的全称,例如Object类对应的全限定名是java/lang/Object)的索引。
  6. 0002代表全限定名常量的索引,0x0002代表着该常量的地址值。
  7. 01表示常量的tag值,去上面表中查找tag值是1的常量类型,是CONSTANT_Utf8_info类型的。从此步开始解析一个。
  8. 001D代表该常量的二进制长度,。是因为CONSTANT_Utf8_info类型的常量的结构由tag、length、bytes构成。所以紧接着01的应该是结构中的length。
  9. 由于001D对应的十进制是29,所以从001D往后的29个字节都是该类的全限定名的字符串所对应的二进制。
  10. 根据上面的过程,可以将整个常量池中的常量都解析出来。

通过java的反编译,可以看到class文件里面的常量池如下:
在这里插入图片描述
注意:仔细看一下会发现,其中有一些常量似乎从来没有在代码中出现过,如“I”、“V”、“”、“LineNumberTable”、“LocalVariableTable”等。这部分自动生成的常量的确没有在Java代码里面直接出现过,但它们会被后面即将讲到的字段表(field_info)、方法表(method_info)、属性表(attribute_info)引用到,它们会用来描述一些不方便使用“固定字节”进行表达的内容。譬如描述方法的返回值是什么?有几个参数?每个参数的类型是什么?因为Java中的“类”是无穷无尽的,无法通过简单的无符号字节来描述一个方法用到了什么类,因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达。

访问标志
这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。

在这里插入图片描述

它的ACC_PUBLIC、ACC_SUPER标志应当为真,而ACC_FINAL、
ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM这6个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。如下:

在这里插入图片描述

类索引
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
以下面这个为例:3个u2类型的值分别为0x0001、0x0003、0x0000,也就是类索引为1,父类索引为3,接口索引集合大小为0。

在这里插入图片描述

字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。

在这里插入图片描述

字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型,其中可以设置的标志位和含义见表6-9。

在这里插入图片描述

跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。

  • 类全限定名:是把类全名中的“.”替换成“/”。例如:org/fenixsoft/clazz/TestClass
  • 简单名:inc()方法和m字段的简单名称分别是“inc”和“m”。
  • 描述符:描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。基本数据类型以及代表无返回值的void类型都用一个大写字符来表示。
    在这里插入图片描述
  • 对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为:“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录为“[I”。
方法
int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)

的描述符为“([CII[CIII)I”

在这里插入图片描述

  • 字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
方法表集合

Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项,见表6-11。
在这里插入图片描述

在这里插入图片描述

以一个方法解析为例:
在这里插入图片描述

  • 0002,代表集合中有两个方法(这两个方法为编译器添加的实例构造器和源码中的方法inc())。
  • 0001,表示ACC_PUBLIC标志为真。
  • 0007,表示方法名称的字符串索引值。对应反编译里面的<init>
  • 0008,表示描述符索引值(方法的返回值)。对应反编译里面的()V
  • 0001就表示此方法的属性表集合有一项属性,属性名称索引为0x0009,对应常量为“Code”,也就是方法体的部分

如果父类方法在子类中没有被重写(Override)。方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“”方法和实例构造器“” 方法。

补充:在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名 ,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。

属性表集合
属性表(attribute_info)在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。前面在提到方法表集合时,提到了方法的权限、描述符、名称等。确没有提到方法的真正代码。方法的执行代码在方法表中的attribute_info中以Code的形式被描述。attribute_info还可以做如下选择。

在这里插入图片描述

续上表:
在这里插入图片描述
续上表:
在这里插入图片描述
续上表:
在这里插入图片描述

对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。下面以依次说明:

  1. Code属性:Code指的就是方法体里面的Java代码。Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。,如果方法表有Code属性存在,那么它的结构将如表6-15所示:在这里插入图片描述
  • attribute_name_index是方法名。
  • attribute_length指示了属性值的长度。
  • max_stack代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。
  • max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。
  • code_length和code用来存储Java源程序编译后生成的字节码指令。
  • 关于code_length,有一件值得注意的事情,虽然它是一个u4类型的长度值,理论上最大值可以达到2 -1,但是虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令,即它实际只使用了u2的长度,如果超过这个限制,Javac编译器也会拒绝编译。

如图6-10所示,这是上一节分析过的实例构造器“”方法的Code属性。它的操作数栈的最大深度和本地变量表的容量都为0x0001,字节码区域所占空间的长度为0x0005。虚拟机读取到字节码区域的长度后,按照顺序依次读入紧随的5个字节,并根据字节码指令表翻译出所对应的字节码指令。翻译“2A B700 0A B1”的过程为:

  • 读入2A,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
  • 读入B7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。
  • 读入000A,这是invokespecial的参数,查常量池得0x000A对应的常量为实例构造器“<init>”方法的符号引用。
  • 读入B1,查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。
    在这里插入图片描述

在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。

  1. Exceptions属性:Exceptions属性是在方法表中与Code属性平级的一项属性。Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常。
  2. LineNumberTable属性:LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。
  3. LocalVariableTable属性:LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。
  4. SourceFile属性:SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以分别使用Javac的-g:none或-g:source选项来关闭或要求生成这项信息。在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。
  5. ConstantValue属性:ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。
  6. InnerClasses属性:InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。
  7. Deprecated及Synthetic属性:Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@deprecated注释进行设置。编译器通过生成一些在源代码中不存在的synthetic方法和类的方式,实现了对private级别的字段和类的访问,从而绕开了语言限制,这可以算是一种trick。在实际生产和应用中,基本不存在程序员需要考虑synthetic的地方。
  8. StackMapTable属性:这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
  9. Signature属性:任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。之所以要专门使用这样一个属性去记录泛型类型,是因为Java语言的泛型采用的是擦除法实现的伪泛型,在字节码(Code属性)中,泛型信息编译(类型变量、参数化类型)之后都通通被擦除掉。使用擦除法的好处是实现简单(主要修改Javac编译器,虚拟机内部只做了很少的改动)、非常容易实现Backport,运行期也能够节省一些类型所占的内存空间。 运行期做反射时无法获得到泛型信息。Signature属性就是为了弥补这个缺陷而增设的,现在Java的反射API能够获取泛型类型,最终的数据来源也就是这个属性。
  10. BootstrapMethods属性:这个属性用于保存invokedynamic指令引用的引导方法限定符。《Java虚拟机规范(Java SE 7版)》规定,如果某个类文件结构的常量池中曾经出现过CONSTANT_InvokeDynamic_info类型的常量,那么这个类文件的属性表中必须存在一个明确的BootstrapMethods属性。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值