虚拟机执行子系统(一):类文件结构

虚拟机执行子系统(一):类文件结构

Class文件结构

  • 一个Class文件对应唯一类或接口,但反过来则不是(譬如类或接口也可以动态生成,直接送入类加载器中)。

  • Class文件是一组以8个字节为基础单位的二进制流,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。需要占用8个字节以上空间的数据项按照高位在前的方式分割(Big-Endian:高字节在低地址,与x86架构相反)。

    Class的结构不像XML等描述语言,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,全部都不允许改变

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

    1. 无符号数:为基本数据类型(以u1u2u4u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。)

    2. :由多个无符号数或者其他表作为数据项的复合数据类型,以_info结尾

      image-20200309164047726.png

    这两种数据类型需要描述数量不定的多个数据的时候,会使用一个前置的容量计数器加上若干个连续数据项的形式,称这一系列连续的某一类型的数据为某一类型的“集合”。

魔数与Class文件版本

  1. 魔数:Class文件的头四个字节,用于标识该文件是否能被JVM接受(表示这个文件是否是.class文件,值为0xCAFEBABE咖啡宝贝)
  2. 版本号:紧跟魔数之后的4个字节,5、6为次版本号,7、8为主版本号(目前只考虑主版本号的话是45.x-57.x),低版本的JVM无法执行高版本的.class文件

常量池

紧跟主次版本号,占用Class文件空间最大的数据项目之一。

  • 入口u2类型的数据——常量池容量计数值(constant_pool_count):从1开始计数,空出第0个常量(目的:后面某些指向常量池的索引值的数据在特定情况下“不引用任何一个常量池项目”),一共有constant_pool_count - 1个常量

  • 存放两大常量:

    1. 字面量

    2. 符号引用:主要包括如下几类常量

      • 被模块导出或者开放的Package

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

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

      • 方法的名称和描述符

      • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)

      • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

        .java文件由javac编译为字节码时,没有进行“连接”步骤,直到JVM加载.class文件时才进行动态连接(把各个方法和字段最终置入内存,即标明这些方法和字段的内存入口地址(符号引用更改为实际的内存地址))

  • 每一项常量都是一个表:

    image-20200309175354484.png

    image-20200309175633693.png

    表内结构:起始第一位是个u1类型的标志位,用于标识当前表对应的常量类型。

    • CONSTANT_Class_info(类/接口名称)

      image-20200309180252724.pngname_index是常量池的索引值,指向常量池中一个CONSTANT_Utf8_info类型常量(代表这个类或者接口的全限定名)

    • CONSTANT_Utf8_info(UTF8编码的字符串类型)

      image-20200309181432249.png

      其中length字段表示的是使用UTF8缩略编码表示的字符串。UTF-8缩略编码与普通UTF-8编码的区别是:
      '\u0001''\u007f'之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,从'\u0080''\u07ff'之间的所有字符的缩略编码用两个字节表示,从'\u0800'开始到'\uffff'之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示

      这个类型的字段由于lengthu2类型,所以他也限制了Java中方法、字段名的最大长度:65535(2字节)

    • 剩下的其他表

      image-20200309182038901.png

      image-20200309182113337.png

      image-20200309182428246.png

    可以使用javap命令来分析字节码文件

    package org.fenixsoft.clazz;
        public class TestClass {
        private int m;
        public int inc() {
        	return m + 1;
        }
    }
    
    C:\>javap -verbose TestClass
    Compiled from "TestClass.java"
    public class org.fenixsoft.clazz.TestClass extends java.lang.Object
    SourceFile: "TestClass.java"
    minor version: 0
    major version: 50
    Constant pool:
    const #1 = class #2; // org/fenixsoft/clazz/TestClass
    const #2 = Asciz org/fenixsoft/clazz/TestClass;
    const #3 = class #4; // java/lang/Object
    const #4 = Asciz java/lang/Object;
    const #5 = Asciz m;
    const #6 = Asciz I;
    const #7 = Asciz <init>;
    const #8 = Asciz ()V;
    const #9 = Asciz Code;
    const #10 = Method #3.#11; // java/lang/Object."<init>":()V
    const #11 = NameAndType #7:#8;// "<init>":()V
    const #12 = Asciz LineNumberTable;
    const #13 = Asciz LocalVariableTable;
    const #14 = Asciz this;
    const #15 = Asciz Lorg/fenixsoft/clazz/TestClass;;
    const #16 = Asciz inc;
    const #17 = Asciz ()I;
    const #18 = Field #1.#19; // org/fenixsoft/clazz/TestClass.m:I
    const #19 = NameAndType #5:#6; // m:I
    const #20 = Asciz SourceFile;
    const #21 = Asciz TestClass.java;
    # 其中#加数字的组合表示符号引用的值
    

    方法描述符:由方法的参数类型以及返回类型所构成。

访问标志

紧跟常量池,长度为两个字节,这些标志用于识别一些类或者接口层次的访问控制信息,包括

  1. 这个Class是类还是接口
  2. 是否是public类型
  3. 是否定义为abstract类型
  4. 如果是类,是否声明为final

image-20200310194750382.png

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

  • 概念

    1. 这三个数据用于确定该类的继承关系
    2. 类索引:用于确定该类的全限定名
    3. 父类索引:确定这个类父类的全限定名,只允许有一个(不允许多继承),除了java.lang.Object外,所有Java类的父类索引都不为0
    4. 接口索引集合:描述该类实现了哪些接口,并按implements关键字(或者当前接口的extends)后的接口顺序从左到右排列在接口索引集合中
  • 类索引和父类索引的查找

    image-20200310200559294.png

    首先从类索引开始,他的值为一个CONSTANT_Class_info的表,其中CONSTANT_Class_info的值指向了一个CONSTANT_Utf8_info的表,这个表中含有类的全限定名

  • 接口索引集合

    入口的第一项u2类型的数据为接口计数器interfaces_count),从0开始计算,后面跟有接口索引表

字段表集合

第一个u2类型的数据表示字段表集合的容量fields_count

  • 概念

    1. 用于描述接口或者类中声明的变量(Java语言中的“字段”(Field)包括类变量、实例变量,但不包括在方法内部声明的局部变量。)

      字段包括的修饰符有:

      1. 字段的作用域publicprivateprotected修饰符)
      2. 是实例变量还是类变量static修饰符)
      3. 可变性final
      4. 并发可见性volatile修饰符,是否强制从主内存读写)
      5. 可否被序列化transient修饰符)
      6. 字段数据类型(基本类型、对象、数组)
      7. 字段名称

      字段的名字、数据类型只能引用常量池中的常量来描述

      上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。

      image-20200310202524476.png

      1. access_flagsu2类型来表示,其标志位如下

        image-20200310202638232.png

      2. name_indexdescriptor_index都是对常量池的引用,分别代表字段的简单名称以及字段的和方法的描述符

        • 简单名称:没有类型参数修饰的方法或者字段名称(比如方法int getNum(Item[] items),简单名称就是getNum,字段private Item item的简单名称就是item

        • 描述符

          1. 字段描述符:字段的数据类型

          2. 方法描述符:方法的参数列表(数量、类型、顺序)+方法返回值先参数列表后返回值存储)

            方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,
            int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I

          3. 基本数据类型以及void用一个大写字符表示、对象类型用L+对象全限定名表示

            image-20200310203526433.png

      3. 后面跟随的属性表集合用于存储额外信息,比如“final static int m=123;”,那就可能会存在一项名称为ConstantValue的属性,其值指向常量123

      4. 有可能出现不在原本Java代码中的字段,比如内部类存有指向外部类实例的引用

方法表集合

  • 概念

    方法表集合和字段表集合十分相似,第一个u2类型的数据表示方法表集合的容量methods_count

    方法表和字段表结构十分相似

    image-20200310204649657.png

    1. access_flags内容如下

      image-20200310204809285.png

    2. 方法中的代码存放在方法属性表集合中一个名为Code的属性里面

    3. 有可能出现不在原本Java代码中的字段,比如类构造器“<clinit>()”和实例构造器“<init>()

    4. Java语言层面的重载,如果方法签名(由方法的名称和参数类型构成)仅仅是返回值不同,也是不允许的,Class文件层面的重载只要字节码方法签名(方法名称、方法参数、方法返回值以及受查异常表)不完全相同即可

属性表集合

JDK12中的预定义属性如下:

image-20200311123130185.png

image-20200311123237781.png

  • 属性的名称:常量池中的一个CONSTANT_Utf8_info类型的常量
  • 属性值的结构:完全自定义,只需要通过一个u4来说明属性值所占位数

image-20200311125034885.png

  1. Code属性

    方法体中的代码编译为字节码后,存储到这个属性中。

    但并非所有的方法表都有这个属性,例如接口或者抽象类的方法就不存在该属性。

    image-20200311125619399.png

    • attribute_name_index属性名,指向一个CONSTANT_Utf8_info常量,值固定为Code

    • attribute_length属性值长度

    • max_stack操作数栈的最大深度,JVM根据这个值分配栈帧的中操作栈的深度

    • max_locals局部变量表所需的存储空间,单位是变量槽(Slot)。

      1. 基本数据类型和returnAddress这些长度不超过32bit的:1 Slot
      2. double、long,64bit:2 Slot

      总的来说,方法参数(包括实例隐藏参数this)、显式异常处理程序参数(catch的参数)、方法体中定义的局部变量都需要局部变量表来存放。

      JVM将局部变量表中的变量槽进行重用:超出作用域时,新的局部变量可以使用以前局部变量的槽。

      Javac编译器根据同时生存的最大局部变量数量和类型计算出max_locals

    • code_length字节码长度u4类型,但是《JVM规范》规定一个方法不能超过65535条指令,实际只能使用u2的长度

    • code字节码指令,每个指令为u1类型的单字节(最多可以表示256条指令)

      package org.fenixsoft.clazz;
      public class TestClass {
          private int m;
          public int inc() {
            	return m + 1;
          }
      }
      

      上面的程序在字节码中是这样的

      image-20200311132609292.png

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

      从上述过程可以看出,JVM执行字节码基于栈的体系结构

      // 原始Java代码
      public class TestClass {
          private int m;
          public int inc() {
          	return m + 1;
          }
      }
      C:\>javap -verbose TestClass
      // 常量表部分的输出省略掉
      {
      public org.fenixsoft.clazz.TestClass();
          Code:
              Stack=1, Locals=1, Args_size=1 #//在这里Locals=1和Arg_size=1是因为实例方法含有this指针
              0: aload_0
              1: invokespecial #10; //Method java/lang/Object."<init>":()V
              4: return
      LineNumberTable:
      	line 3: 0
      LocalVariableTable:
      	Start Length Slot Name Signature
      	0     5      0    this Lorg/fenixsoft/clazz/TestClass;
      public int inc();
          Code:
              Stack=2, Locals=1, Args_size=1
              0: aload_0
              1: getfield #18; //Field m:I
              4: iconst_1
              5: iadd
              6: ireturn
      LineNumberTable:
      	line 8: 0	
      LocalVariableTable:
      	Start Length Slot Name Signature
      	0     7      0    this Lorg/fenixsoft/clazz/TestClass;
      }
      
    • 显式异常处理表:不一定存在

      image-20200311134549892.png

      表示从start_pcend_pc之间,如果出现了类型为catch_type(指向一个CONSTANT_Class_info型常量)或者其子类的异常,则转到handler_pc行继续处理。特别的,当catch_type == 0时,任何异常都转到handler_pc行继续处理。

      Java异常处理

      1. finally中的代码总会被执行。
      2. trycatch中有return时,也会执行finallyreturn的时候,要注意返回值的类型,是否受到finally中代码的影响。
      3. finally中有return时,会直接在finally中退出,导致trycatch中的return失效。
  2. Exceptions属性

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

    • 结构

      image-20200311140657028.png

      • number_of_exceptions可能抛出的异常种类数量
      • exception_index_table异常类型,指向常量池中CONSTANT_Class_info型常量
  3. LineNumberTable属性

    • 作用:用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。不是运行时必需的(默认生成到class文件中),主要用于调试

    • 结构

      image-20200311151503599.pngline_number_info:包含start_pc(字节码行号)与line_number(Java源码行号)两个u2数据项

  4. LocalVariableTable及LocalVariableTypeTable属性

    • LocalVariableTable作用:描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性(默认生成到class文件中),也是跟调试有关,如果没有这个属性,则无法通过参数名称查找参数。

    • LocalVariableTable结构

      image-20200311152404827.png

      local_variable_info项目代表了一个栈帧与源码中的局部变量的关联

      image-20200311152529423.png

      • start_pc:这个局部变量的生命周期开始的字节码偏移量

      • length:该局部变量作用范围长度

        两者合起来便是该局部变量的作用域

      • name_index:局部变量的名称,常量池中CONSTANT_Utf8_info型常量

      • descriptor_index:局部变量的描述符,常量池中CONSTANT_Utf8_info型常量

      • index:局部变量在栈帧的局部变量表中变量槽的位置。

    • LocalVariableTypeTable:和LocalVariableTable很相似,只是把descriptor_index替换为了字段特征签名Signature,对于非泛型类型,这个改动没有影响;对于泛型类型,必须使用这个属性以保存泛型的参数化类型。

  5. SourceFile及SourceDebugExtension属性

    • SourceFile作用记录生成这个Class文件的源码文件名称,对于内部类其源码文件名和类名不一定相同

    • SourceFile结构

      image-20200312150717217.png

    • SourceDebugExtension作用存储额外的代码调试信息,典型的场景是在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号。跟JSR45有关,主要用于定位非Java语言但需要用JVM运行的程序。

  6. ConstantValue属性

    • 作用通知JVM自动为静态变量(static修饰)赋值

      实例变量和静态变量的赋值时刻不同:

      • 实例变量:在实例构造器<init>()中进行
      • 静态变量:如果是基本类型或者String类型,且有final修饰,则自动生成ConstantValue属性来初始化;否则(不满足上述两个条件任一),则会在类构造器<clinit>()进行
    • 结构

      image-20200312153550174.png

      其中attribute_length是固定值,为2(后面的属性长度),constantvalue_index是常量池中一个字面量的引用(基本类型到CONSTANT_String_info

  7. InnerClasses属性

    • 作用记录内部类和宿主类之间的关联

    • 结构

      8eyaBF.png

      • number_of_classes内部类的数量

      • inner_classes_info内部类的信息表

        8eyUnU.png

        inner_class_info_indexouter_class_info_index都是指向常量池中CONSTANT_Class_info型常量的索引,分别代表了内部类和宿主类的符号引用。

        inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称,如果是匿名内部类,这项值为0。

        inner_class_access_flags是内部类的访问标志,类似于类的access_flags

        8eytXT.png

  8. Deprecated及Synthetic属性

    都是布尔属性(要么有则为true,要么根本没有,可以看作是false)。

    • Deprecated:通过@deprecated配置
    • Synthetic:代表此字段不是Java源码产生,而是由编译器自动添加的

    8eyYcV.png

    attribute_length0x00000000

  9. StackMapTable属性

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

  10. Signature属性

    是一个可选的定长属性,可以出现于字段表方法表结构的属性表中。

    • 作用用于记录泛型签名信息,因为Java的泛型会擦除,这个属性使得Java支持通过反射获取泛型类型

    • 结构

      8eyJ10.png

      signature_index:对常量池的索引(CONSTANT_Utf8_info),表示类签名或方法类型签名或字段类型签名(具体是哪个的签名,取决于Signature属性是哪个表的属性)

  11. BootstrapMethods属性

    是一个复杂的变长属性,位i于类文件的属性表中。

    • 作用:保存invokedynamic指令引用的引导方法限定符。(跟反射有关)

    • 结构

      8eyGpq.png

      8eyunS.png

  12. MethodParameters属性

    是一个用在方法表中的变长属性。

    • 作用记录方法的各个形参名称和信息,因为接口和抽象方法没有Code属性,Javac加上-parameters参数即可。

    • 结构

      8eyZ1P.png

      8eyVpt.png

      name_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,代表了该参数的名称。而access_flags是参数的状态指示器,它可以包含以下三种状态中的一种或多种:

      • 0x0010ACC_FINAL):表示该参数被final修饰。
      • 0x1000ACC_SYNTHETIC):表示该参数并未出现在源文件中,是编译器自动生成的。
      • 0x8000ACC_MANDATED):表示该参数是在源文件中隐式定义的。Java语言中的典型场景是this关键字。
  13. 模块化相关属性

    模块描述文件(module-info.java)最终是要编译成一个独立的Class文件来存储的,所以,Class文件格式也扩展了Module、ModulePackages和ModuleMainClass三个属性用于支持Java模块化相关功能。

  14. 运行时注解相关属性

    一共有6个属性用于存储注解信息。

    RuntimeVisibleAnnotations:记录了类、字段或方法的声明上记录运行时可见注解,当我们使用反射API来获取类、字段或方法上的注解时,返回值就是通过这个属性来取到的。

    8eyM7Q.png

    8ey1ts.png

    type_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,该常量应以字段描述符的形式表示一个注解。

    num_element_value_pairselement_value_pairs数组的计数器,element_value_pairs中每个元素都是一个键值对,代表该注解的参数和值。

发布了24 篇原创文章 · 获赞 0 · 访问量 982

猜你喜欢

转载自blog.csdn.net/deltapluskai/article/details/104897868