[深入理解Java虚拟机] 第6章 类文件结构

文章目录

类文件的结构

  • Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

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

  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表

1. 魔数与Class文件的版本

  • 每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。值为:0xCAFEBABE

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

2. 常量池

  • 在常量池的入口放置一项u2类型的数据,代表常量池容量计数值,容器计数从1开始(某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示)

  • 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)

    • 字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用包括了下面三类常量:
      • 类和接口的全限定名(Fully Qualified Name)
      • 字段的名称和描述符(Descriptor)
      • 方法的名称和描述符
  • 有14中常量类型,每种均有自己的结构

由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译

3. 访问标志

常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等

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

  • 类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。

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

  • 对于接口索引集合,入口的第一项——u2类型的数据为接口计数器(interfaces_count),表示索引表的容量

5. 字段表集合

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

  • 对于数组类型,每一维度将使用一个前置的“[”字符来描述

  • 第一个u2类型的数据为容量计数器fields_count,每个字段有对应的表结构:access_flags/name_index/descriptor_index/attributes_count/attributes

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

  • 另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,
    那字段重名就是合法的。

全限定名和简单名称很好理解,“org/fenixsoft/clazz/TestClass”是这个类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。简单名称是指没有类型和参数修饰的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分别是“inc”和“m”。

6. 方法表集合

  • 方法表的结构如同字段表一样,因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对的,synchronized、native、strictfp和abstract关键字可以修饰方法,所以方法表的访问标志中增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。

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

在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。

7. 属性表集合

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

7.1. Code属性

  • 出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性

  • 在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据

  • 在字节码指令之后的是这个方法的显式异常处理表(下文简称异常表)集合,异常表对于Code属性来说并不是必须存在的

7.2. Exceptions属性

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

7.3. LineNumberTable属性

描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系

7.4. LocalVariableTable属性

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

  • 如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失

7.5. SourceFile属性

用于记录生成这个Class文件的源码文件名称,这个属性也是可选的

7.6. ConstantValue属性

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

对于static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性。目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化。

7.7. InnerClasses属性

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

7.8. Deprecated及Synthetic属性

  • Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。其中attribute_length数据项的值必须为0x00000000,因为没有任何属性值需要设置

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

  • Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的。

  • 所有由非用户代码产生的类、方法及字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器“<init>”方法和类构造器“<clinit>”方法。

7.9. StackMapTable属性

  • 一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

  • StackMapTable属性中包含零至多个栈映射帧(Stack Map Frames),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。

7.10. Signature属性

  • 一个可选的定长属性,可以出现于类、属性表和方法表结构的属性表中

  • ,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。

7.11. BootstrapMethods属性

用于保存invokedynamic指令引用的引导方法限定符(第8章)

字节码指令

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作
码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构
成。

1. 字节码与数据类型

  • 对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference

  • 大部分的指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型。编译器会在编译期或运行期将byte和short类型的数据带符号扩展(Sign-Extend)为相应的int类型数据,将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据。与之类似,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。因此,大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型(Computational Type)。

2. 加载和存储指令

  • 将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。

  • 将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。

  • 将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。

  • 扩充局部变量表的访问索引的指令:wide。

3. 运算指令

  • 加法指令:iadd、ladd、fadd、dadd。
  • 减法指令:isub、lsub、fsub、dsub。
  • 乘法指令:imul、lmul、fmul、dmul。
  • 除法指令:idiv、ldiv、fdiv、ddiv。
  • 求余指令:irem、lrem、frem、drem。
  • 取反指令:ineg、lneg、fneg、dneg。
  • 位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
  • 按位或指令:ior、lor。
  • 按位与指令:iand、land。
  • 按位异或指令:ixor、lxor。
  • 局部变量自增指令:iinc。
  • 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。

4. 类型转换指令

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

    • int类型到long、float或者double类型。
    • long类型到float、double类型。
    • float类型到double类型。
  • 处理窄化类型转换(Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成,这些转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。

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

    • 如果浮点值是NaN,那转换结果就是int或long类型的0。
    • 如果浮点值不是无穷大的话,浮点值使用IEEE 754的向零舍入模式取整,获得整数值v,如果v在目标类型T(int或long)的表示范围之内,那转换结果就是v。
    • 否则,将根据v的符号,转换为T所能表示的最大或者最小正数。
  • 窄化转换指令永远不可能导致虚拟机抛出运行时异常。

5. 对象创建与访问指令

  • 创建类实例的指令:new。

  • 创建数组的指令:newarray、anewarray、multianewarray。

  • 访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield、getstatic、putstatic。

  • 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。

  • 将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。

  • 取数组长度的指令:arraylength。
    检查类实例类型的指令:instanceof、checkcast。

6. 操作数栈管理指令

  • 将操作数栈的栈顶一个或两个元素出栈:pop、pop2。

  • 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。

  • 将栈最顶端的两个数值互换:swap。

7. 控制转移指令

控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序

  • 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。

  • 复合条件分支:tableswitch、lookupswitch。

  • 无条件分支:goto、goto_w、jsr、jsr_w、ret。

8. 方法调用和返回指令

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

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

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

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

  • invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

9. 异常处理指令

  • 在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现

  • 当除数为零时,虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常。而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成的。

10. 同步指令

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

  • 编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须执行其对应的monitorexit指令,而无论这个方法是正常结束还是异常结束。

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/Blanchedingding/article/details/86000075
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞