《深入了解Java虚拟机》第六章

第六章 类文件结构

Java虚拟机优势:

  1. java虚拟机可以运行在各种不同硬件平台和操作系统上,这些虚拟机可以载入和执行同一种平台无关的字节码(所有平台统一支持的程序存储格式),从而实现“一次编写,到处运行”。
  2. 实现语言无关性。java虚拟机只与class这种二进制文件格式关联。Class文件中包含了Java虚拟机指令集、符号表、若干其他辅助信息。虚拟机不关心是什么语言被转换成Class文件。Class文件是jvm执行引擎的数据入口。
    在这里插入图片描述

class类文件的结构

· 任意一个Class文件都存储了一个类或者接口的定义信息。但是反过来,类和接口并不一定都定义在文件中。
· Class文件是以8字节为基础单位的二进制流,文件中没有分隔符。当遇到占用8字节以上的数据项时候,会按照高位在前分割成若干8字节进行存储。
· Class文件格式有两种数据类型“无符号数”和“表”。无符号数是基本数据类型,而表是由无符号数或者表作为数据项构成的复合数据类型,所有表的命名习惯以“_info”结尾。

  1. 每个Class文件头4字节称“魔数”,值为“0xCAFEBABE”确定这个文件是否为一个能被虚拟机接受的class文件。
  2. 接着四个字节存储Class文件的版本号:5,6为此版本,7,8为主版本号。
  3. 接着是常量池入口,通常是占用Class文件空间最大的数据项目之一。是表类型数据项目。放置一个u2类型数据代表常量池容量计数值(class文件结构只有它是从第1项开始计数,其他都是第0项),如果不引用任何一个常量池项目值就设为0。
    • 常量池主要存放:字面量和符号引用。符号引用包括:包,类和接口的全限定名,方法名称和描述,字段名称和描述,方法句柄和方法类型,动态调用点和动态常量。
    • Class文件不会直接保存各个方法、字段最终在内存中的布局信息,而是当虚拟机做类加载时从常量池中获得对应符号引用,再在类创建或者运行的时候解析、翻译到具体的内存地址中。
    • 常量池中每一项常量都是一个表。开始为11种表结构数据,为了更好支持动态语言调用,增加4种,之后为了支持java模块化系统,又增加2种。至JDK13有17种。
    每种结构第一位都是u1类型的标志位,区分常量类型。
  4. 接着的两个字节代表访问标志。用于识别一些类或者接口层次的访问信息,包括:是接口还是类,是否定义为public,是否定义为abstract等。
  5. 紧接着,类索引父类索引是一个u2的数据,接口索引集合是一组u2类型的集合,这三种数据确定该类型的继承关系。
    类索引用于确定这个类的全限定名,而父类索引除了Object类外,所有的java类的父类索引都只有一个,且不为0。类索引、父类索引各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过索引值找到定义到CONSTANT_Utf8_info类型的常量中的全限定名字符串。
    在这里插入图片描述
    接口索引集合则描述这个类实现了哪些接口。(按从左到右的顺序排列到集合中)入口的第一项u2的数据为接口计数器,表示索引表的容量。如果没有实现任何接口值为0。
  6. 字段表用于描述接口或者类中声明的变量。字段修饰符放在access_flags的u2的数据中。之后是name_index和descriptor_index,它们是对常量池的引用,分别代表字段的简单名称及字段和方法的描述符。这三个为固定数据项目。
    在这里插入图片描述
    字段表集合入口第一个u2类型的数据为容量计数器fields_count,说明字段表数据个数。之后就是access_flags,name_index,descriptor_index。之后跟随着一个属性表集合,用于存储一些额外的信息。

Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

全限定名,简单名称,描述符概念
全限定名就是把类全名中的“.”换成”/“,最后以“;”表示结束。
简单名称指没有类型和参数修饰的方法或者字段名称。
(字段在Java中,包括类级变量以及实例级变量)
描述符用来描述字段的数据类型、方法的参数列表(包括数量、类型、顺序)和返回值。

在这里插入图片描述

对于数组类型,一维使用一个前置”[“描述,二维用两个”[“。例如,int[]将记录成“[I”。描述方法时,参数列表按照顺序放在括号内。如int indexOf(char[]source,intsourceOffset,int sourceCount,char[]target,int targetOffset,inttargetCount,int fromIndex)的描述符为“([CII[CIII)I”

字段表不会列出继承而来的字段,但可能还出现代码中不存在的字段。另外在java语法中,字段无法重载,但在class文件中,只要两字段描述符不完全相同,字段重名就合法。

7.class文件中对方法的描述与对字段的描述几乎一样,方法表中依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。
在这里插入图片描述
方法中的代码经编译成字节码指令后,被存放在方法属性表集合中的一个叫“Code”的属性里面。属性表集合是class文件格式中最具扩展性的一种数据项目。

 方法表集合入口第一个u2类型的数据为容量计数器fields_count,代表集合中有几个方法。之后为access_flags,name_index,descriptor_index。接下来属性表计数器attributes_count的值表示此方法的属性表集合有几项属性,属性名称的索引值有个对应常量,例如0x0009,对应常量为“Code”,说明此属性是方法的字节码描述。
 与字段表相对于的,如果父类方法在子类中没有被重写,那么方法表集合中不会出现来自父类方法的信息。但也会出现编译器自动添加的方法,最常见的是类构造器“()”方法和实例构造器“()”。
java中重载一个方法:

  1. 方法名相同
  2. 拥有一个与原方法不同的特征签名(指一个方法中各个参数在常量池中的字段符号引用的集合,不包括返回值)
    但是class文件格式中,特征签名的范围更大,只要描述符不完全一致就可以共存。

属性表集合

class文件、字段表、方法表都可以携带自己的属性表集合,描述某些场景专有的信息。

属性表集合限制较宽松,不需要属性表具有严格顺序,只要不与已规定属性名重复,任何人实现的编译器可以向属性表中写入自己定义的属性信息,jvm会自动忽略认不得的属性,所有jvm都能识别的属性已有29项。每个属性名称都是从常量池中引用一个CONSTANT_Utf8_info类型的常量表示,而属性值的结构都是自定义的,在之前只需要一个u4的数据项来说明长度即可。
在这里插入图片描述

  1. Code属性
    java经编译后的方法体变成字节码指令存储在这里面。不是每个方法表都需要该属性。如果存在该属性,则code属性表结构为:
    在这里插入图片描述
    · attribute_name_index指向常量池的CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,代表属性的属性名称
    · attribute_length指示了属性值的长度
    · max_stack代表了操作数栈的最大深度值
    · max_locals代表了局部变量表所需要的存储空间。单位是“变量槽slot”(除了double、long占2变量槽,其他长度不超过32位的数据类型每个局部变量都占一个),变量槽是虚拟机为局部变量分配内存最小单位。局部变量表中存放:方法参数(包括隐藏this),try-catch的catch块定义的异常处理参数,方法体中的局部变量。JVM会重用变量槽,所以运行过程中同时生存的最大局部变量数量和类型来计算max_locals大小
    · code_length和code存储的是字节码指令信息, code_length表示字节码长度。虽然是u4类型,但是JVM规定一个方法长度不超过(65535,u2的长度),超过就拒绝编译。每条指令是一个u1类型的单字节,jvm访问到这个单字节就能知道对应的指令、是否需要参数、参数如何解析等信息。code可以一共表达256条指令,不过目前只有200多条
    · 之后的异常表集合并不是必须存在的。规定使用异常表来实现java异常及finally处理机制
    如果把一个java程序分为代码(方法体中的代码)和元数据(包括类、字段、方法定义及其他),那么Code属性描述代码,其他属性都描述元数据。

  2. Exceptions属性

    方法表中与Code属性平级的一个属性,与异常表不同。作用是列举出可能抛出的受查异常

    在这里插入图片描述
    number_of_exceptions项表示受查异常的个数,每一种异常使用一个exception_index_table项表示。exception_index_table是指向常量池中CONSTANT_Class_info型常量的索引,表示该异常的类型。

  3. LineNumberTable属性(可选)
    描述源代码行号和字节码行号之间的对应关系。
    在这里插入图片描述
    line_number_table是一个长度为line_number_table_length,类型为line_number_info的集合。line_number_info表包含start_pc和line_number两个u2类型的数据项,分别代表字节码行号,和源码行号。

  4. LocalVariableTable及LocalVariableTypeTable属性(可选)
    Code属性的子属性。用于描述栈帧中局部变量表的变量与java源码中定义的变量之间的关系。
    在这里插入图片描述
    其中local_variable_info项目代表了一个栈帧和源码中的局部变量的关联。
    在这里插入图片描述
    · Start_pc和length分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度。结合起来就是这个局部变量在字节码中的作用范围。
    · name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了这个局部变量的名称和描述符。
    · index是这个局部变量在栈帧的局部变量表中变量槽的位置。如果数据类型为64位,那么占用index,index+1两个变量槽。

    JDK5引入泛型之后,增加了一个LocalVariableTypeTable属性。仅仅是把记录的字段描述符的descriptor_index替换成了字段的特征签名(Signature)。对于非泛型,描述符和特征签名信息吻合,对于泛型,描述符不能准确描述泛型类型,需要用特征签名。

  5. SourceFile及SourceDebugExtension属性(可选)
    SourceFile属性记录Class文件对应的源码文件名。java中多数类名与文件名一致,也有例外(例如内部类)
    在这里插入图片描述
    sourcefile_index数据项是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名。
    JDK 5时,新增了SourceDebugExtension属性用来存储程序员新增的自定义额外代码调试信息。一个类最多只允许存在一个这个属性。

  6. ConstantValue属性
    通知JVM自动为静态变量赋值。
    对于不用static修饰的实例变量的赋值是在实例构造器()方法中进行的;对于类变量,可以使用类构造器()方法,也可以使用ConstantValue属性。Oracle公司的javac编译器的选择是:如果用了final和static修饰,并且数据类型为基本类型或者String的话,就会生成ConstantValue属性进行初始化;否则就选择()方法初始化。
    ConstantValue属性只支持基本数据类型和字符串类型的原因:此属性的属性值是一个常量池的索引号,而class文件中常量类型只有与基本数据类型和字符串相对应的字面量。
    在这里插入图片描述
    此属性是个定长属性,所以attribute_length数据项值必须固定为2。constantvalue_index数据项代表了常量池中一个字面量常量的引用,可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info和CONSTANT_String_info常量中的一种。

  7. InnerClasses属性(可选)
    记录内部类与宿主类间的关系。如果一个类中有内部类,编译器会自动生成该属性。
    在这里插入图片描述

    · inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_Class_info型常量的索引,代表内部类和外部类的符号引用。

    · inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称,匿名类值为0.
    · inner_class_access_flags是内部类的访问标志,值代表不同含义,如是否为public,是否为接口等等。

  8. Deprecated及Synthetic属性
    两个属性都是属于标志类型的布尔属性。没有属性值需要设置,attribute_length数据项的值必须为0x00000000。
    Deprecated用于表示某个类、字段、方法,不再推荐使用。
    Synthetic属性代表此字段或者方法是由编译器自行添加的,实现越权访问等功能,是早期优化的技巧。jdk5之后可以通过设置ACC_SYNTHETIC标志位来完成Synthetic属性的功能。

  9. StackMapTable属性
    JDK6增加进入Code属性的属性表中,是一个变长属性。在虚拟机加载字节码验证阶段被新类型检查验证器使用,代替之前消耗性能的基于数据流分析的类型推导验证器。新的验证器省去了推导验证器在运行期的通过数据流分析字节码行为逻辑合理性的步骤,改成直接在编译阶段将一系列验证类型记录到class文件中,通过检查这些验证类型代替类型推导过程。
    每个StackMapTable中包含0到多个栈映射帧(Stack Map Frame),每个帧都显示或隐式代表了一个字节码的偏移量,用于表示执行到该字节码时局部变量表和操作数栈的验证类型。
    对于版本号>=50的class文件,如果code属性没有附带该属性,就认为携带一个隐式的StackMap属性。这个属性相当于number_of_entries值为0的StackMapTable属性。一个方法最多只能有一个StackMapTable属性。

  10. Signature属性(可选)
    JDK5加入的一种变长属性,可以出现在类、字段表、方法表结构的属性表中。Java使用的是擦除式的“伪泛型”,就是当编译完成时直接清除掉泛型的信息(类型变量、参数化类型)。优点是简单,在运行时能够节约内存空间,但是缺点是运行的时候无法像c#等有真泛型支持的语言那样,把泛型类型与用户定义的普通类型同等对待,例如运行期间不能获得泛型信息。Signature属性就是为了弥补这个缺陷,记录泛型签名信息。现在java的反射API最终能够获得泛型信息,数据也就是来源这个属性。
    在这里插入图片描述
    Signature_index项的值为指向常量池的CONSTANT_Utf8_info结构的索引。如果当前signat属性是类文件/方法表/字段表的属性,则CONSTANT_Utf8_info结构表示类签名/方法类型签名/字段类型签名。

    11. BootstrapMethods属性

    Jdk7新增的一个变长属性,位于类文件的属性表中。这个属性用于保存InvokeDynamic指令引用的引导方法限定符。
    规定如果常量池中曾经出现过CONSTANT_InvokeDynamic_info类型的常量,那么这个类文件属性表就需要有一个BootstrapMethods属性(有且只有一个)。

  11. MethodParameters属性
    jdk8时新加入class文件,用在方法表中的变长属性。用来记录方法的各个形参名称和信息。LocalVariableTable属性如果没有方法体就没有局部变量表,就不能保存方法参数名称。而MethodParameters属性
    与code属性平级,可以运行时通过反射API获取。

  12. 模块化相关属性
    jdk9的重量级功能为java模块化功能。因为模块描述文件(module-info.java)最终要以模块形式编译成独立文件,所以class文件格式也扩展了Module、ModulePackages和ModuleMainClass三个属性来支持模块化功能。
    · module属性是一个变长属性,表示该模块的名称、版本、标志信息、模块导出包信息等内容。
    · ModulePackages属性是一个变长属性,描述的是模块中所有的包(无论是export或者open的)。
    · ModuleMainClass属性是一个定长属性,用于确定该模块的主类(Main Class)

  13. 运行时注解相关属性
    JDK5,提供对注解的支持。为了存储注解信息,Class文件中增加了RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations和RuntimeInvisibleParameter-Annotations四个属性。JDK8,进一步加强注解的使用范围,新增注解类型,class文件同步增加了RuntimeVisibleTypeAnnotations和RuntimeInvisibleTypeAnnotations两个属性。
    这六个属性很相似,以RuntimeVisibleAnnotations为代表进行介绍。一个变长属性,它记录了类、字段或方法的声明上记录运行时可见注解。
    在这里插入图片描述
    num_annotations是annotations数组的计数器,每个数组元素代表一个运行时可见注解。注解在class文件中以annotation的结构来存储。
    在这里插入图片描述
    · type_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,常量以字段描述符来表示一个注解。num_element_value_pairs是element_value_pairs数组的计数器,element_value_pairs中每个元素为一个键值对,代表该注解的参数和值。

字节码指令简介

jvm的指令由一个字节长度的数字(操作码opcode)以及之后0到多个的操作所需的参数(操作数operand)构成。由于jvm采用面向操作数栈的架构,所以大多数指令不包含操作数,操作数都存放在操作数栈中。
因为class文件格式放弃了编译后操作数长度对齐,意味着jvm处理超过1字节的数据时,不得不在运行时从字节中重建出具体的数据结构。这种操作会导致解释执行字节码时将损失一些性能,也有优势:可以省略大量填充和间隔符,获得精干的编译代码。

字节码与数据类型

大多数指令包含其操作对象所对应的数据类型信息。两条操作数据类型不同的指令,在虚拟机内部可能会是由同一段代码来实现,但在class文件中它们必须拥有各自独立的操作码。
指令中有特殊的字符来表明为哪种数据类型服务:符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。还有的指令并没有用特殊字符来指明,因为它只为一种数据服务。
但是如果每种数据类型和每个操作都有对应的指令,操作码只有一个字节数量太少。所以指令集设计成非完全独立的。利用替换指令模板中的T,得到一个具体的字节码指令。
大部分指令不支持byte、char、short类型,没有任何指令支持Boolean类型。编译器会在编译或者运行期间扩展为int型数据,将Boolean和char类型零位扩展为int数据类型。在处理这四类数组时,也会转换成int型数组

字节码指令

在这里插入图片描述

公有设计,私有实现

· JVM应有的共同程序存储格式:class文件格式以及字节码指令集。这些内容与硬件、操作系统、JVM实现之间都是完全独立的。
· JVM都必须实现:1.能够读取class文件;2.精确实现包含在其中的代码语义。一个优秀的JVM,在约束内,对具体实现进行修改优化是可行的,在后台如何处理class文件是实现者自己的事,只需要外部接口与规范保持一致。不必完全逐字实现《java虚拟机规范》。
· 根据虚拟机实现者关注的是性能、内存消耗、可移植性,使用上面的伸缩性来获得。虚拟机实现的方法主要有以下两种:

  1. 将输入的jvm代码在加载或者执行时翻译成另一种虚拟机的指令集;
  2. 将输入的jvm代码在加载或者执行时翻译成宿主机处理程序的本地指令集(即时编译器代码生产技术)

Class 文件结构的发展

· Class 文件结构一直处于相对稳定的状态,改进基本集中于可扩展数据结构中新增内容(访问标志、属性表等)。
· Class 文件格式所具备的平台中立、紧凑、稳定、可扩展的特点,是java技术体系实现平台无关、语言无关的重要支柱。