问题

其他语言是否可以使用 java 虚拟机作为产品交付媒介?

是可以的,除了 java 外,Jruby, Groovy, Scala 都有自己的编译器编译成字节码*.class 文件,然后通过 Java 虚拟机执行。

Java 文件组成内容

class 文件采用类似于 C 语言结构体的为结构体来存储数据结构

无符号数:就是数值

表 :就是一个结构 XXXX_info{ u4, u2….}

Class 文件格式

class 魔数和版本

u1/u2/u4/u8 代表 字节数。每个 Class 文件的头 4 个自己成为魔数(Magic Number), 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 class 文件。值为 0xCAFEBABE(咖啡宝贝)

Java 的版本号是从 45 开始的,JDK1.1 之后的每个 JDK 大版本发布主版本号向上加 1。
版本号和 Java 编译器的对应关系如下: JDK7 ===> 51. JDK8 == => 52. JDK9 ===> 53; JDK10 == => 54; JDK11 ===> 55

常量池

常量池是 class 文件中非常重要的结构,它描述着整个 class 文件的字面量信息。 常量池是由一组 constant_pool 结构体数组组成的,而数组的大小则由常量池计数器指定。

常量池计数器 constant_pool_count 的值 = constant_pool 表中的成员数+ 1。constant_pool 表的索引值只有在大于 0 且小于 constant_pool_count 时才会被认为是有效的。

constant_pool 是一种表结构, 它包含 Class 文件结构及其子结构中引用的所有字符串常量、 类或接口名、字段名和其它常量。

  • 字面量: 文本字符串、声明为 final 的常量值。如 String str =“atguigu”; final int NUM = 10;

  • 符号引用:接口/类的全限定名 字段的名称和描述符 方法的名称和描述符。

    • pers/fulsun/test/Demo这个就是类的全限定名,仅仅是把包名的“.“替换成“/”,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束

    • 简单名称是指没有类型和参数修饰的方法或者字段名称,类的 add()方法和 num 字段的简单名称分别是 add 和 num。

    • 描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的 void 类型都用一个大写字符来表示,而对象类型则用字符 L 加对象的全限定名来表示

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      标识符  含义
      B 基本数据类型 Byte
      C 基本数据类型 char Unicode字符,UTF-16编码
      D 基本数据类型 double
      F 基本数据类型 float
      I 基本数据类型 int
      J 基本数据类型 long
      S 基本数据类型 short
      Z 基本数据类型 boolean
      V 代表void类型
      L 对象类型,比如 Ljava/lang/Object;
      [ 数据类型,代表数组 int[]数组被记录[I,String[][]二维数组被记录为[[Ljava/lang/String;
    • 方法描述符:按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()之内,如方法:

      1
      2
      3
      String getInfoByIdAndName(int id, String name)
      # 该方法的描述符为:
      (I, Ljava/lang/String;)Ljava/lang/String;

每个常量池项(cp_info) 都会对应记录着 class 文件中的某中类型的字面量。

JVM 虚拟机规定了不同的 tag 值和不同类型的字面量对应关系如下:

根据 cp_info 中的 tag 不同的值,可以将 cp_info 更细化为以下结构体:


常量池信息的轮廓

将 Java 源码编译成*.class 文件后,在此文件的目录下执行 javap -v * 命令

查看真正的字节码文件可以使用 HEXWin、NOTEPAD++、UtraEdit 等工具。

访问标志

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

  • 类的访问权限通为 ACC_开头的常量。比如,若是 public final 的类,则该标记为 ACC_PUBLIC |ACC_FINAL
  • 使用 ACC_SUPER 可以让类更准确地定位到父类的方法 super.method(),现代编译器都会设置并且使用这个标记。
标志类型 对应标志值 标志意义
ACC_PUBLIC 0x0001 是否为 public 类型
ACC_FINAL 0x0010 是否被声明为 final 类型, 只有类可设置
ACC_SUPER 0x0020 是否允许使用 invokespcial 字节码指令的新语义,JDK1.2 之后编译出来的类的这个标志默认是真.(使用增强的方法调用父类方法)
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为抽象类型,对接口或抽象类来说都是为真,其他类型为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码生成(即由编译器产生说的类,没有源码对应)
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

补充说明

  • 带有 ACC_INTERFACE 标志的 class 文件表示的是接口而个是类,反之则表示的是类而个是接口。
    1. 如果一个 class 文件被设置了 ACC_INTERFACE 标志,那么同时也得设置 ACC_ABSTRACT 标志。同时它不能再设置 ACC_FINAL、ACC_SUPER 或 ACC_ENUM 标志。
    2. 如果没有设置 ACC_INTERFACE 标志,那么这个 class 文件可以具有上表中除 ACC_ANNOTATION 外的其他所有标志。当然,ACC_FINAL 和 ACC_ABSTRACT 这类互斥的标志除外。这两个标志不得同时设置。(注解是@interface)
  • ACC_SUPER 标志用于确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义。针对 Java 虚拟机指令集的编译器都应当设置这个标志。对于 JavaSE8 及后续版本来说,无论 class 文件中这个标志的实际值是什么,也不管 class 文件的版本号是多少,Java 虚拟机都认为每个 class 文件均设置了 ACC_SUPER 标志。
    • ACC_SUPER 标志是为了向后兼容由旧 Java 编译器所编译的代码而设计的。目前的 ACC_SUPER 标志在由 JDK1.0.2 之前的编译器所生成的 access flags 中是没有确定含义的,如果设置了该标志,那么 0racle 的 Java 虚拟机实现会将其忽略。
  • ACC_SYNTHETIC 标志意味着该类或接口是由编译器生成的,而不是由源代码生成的。
  • 注解类型必须设置 ACC ANNOTATION 标志。如果设置了 ACC_ANNOTATION 标志,那么也必须设置 ACC INTERFACE 标志。(注解是@interface)
  • ACC_ENUM 标志表明该类或其父类为枚举类型。

类/父类/接口索引

在访问标记后,会使用两个字节分别表示会指定 this_class(类索引)super_class(父类索引)interfaces_count(接口计数器) 以及 interfaces[](接口索引集合)。这三项数据来确定这个类的继承关系。

  • this_class(类索引)
    • 字节无符号整数,指向常量池的索引。它提供了类的全限定名,如 com/atguigu/javal/Demo。this_class 的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为 CONSTANT_Classinfo 类型结构体,该结构体表示这个 class 文件所定义的类或接口。
  • super_class(父类索引)
    • 2 字节无符号整数,指向常量池的索引。它提供了当前类的父类的全限定名。如果我们没有继承任何类,其默认继承的是 java/lang/Object 类。同时,由于 Java 不支持多继承,所以其父类只有一个。
    • superclass 指向的父类不能是 final。
  • interfaces_count(接口计数器)
    • interfaces_count 项的值表示当前类或接口的直接超接口数量。
  • interfaces [](接口索引集合)
    • 由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的 CONSTANT_Class(当然这里就必须是接口,而不是类)。
    • interfaces [] 中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为 interfaces_count。每个成员 interfaces [i] 必须为 CONSTANT_Class_info 结构,其中 0<=i< interfaces_count。在 interfaces [] 中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即 interfaces [0] 对应的是源代码中最左边的接口。

字段表计数器

接口索引集合后边紧跟的结构是字段表计数器,字段表计数器后边紧跟的是字段表。

  • 字段表计数器(fields_count):记录字段表中字段的数量,与其他计数器一样,是一个无符号数结构类型的数据,u2 大小。
  • 字段表(fields):字段表为表类型结构 field_info 。字段表(fields)用于描述接口或者类中声明的变量。字段(field)包括类级变量(即静态变量)以及实例变量(即: 非静态变量),但不包括在方法内部声明的局部变量。简单的总结这句话的意思是字段表中存储的是全局标量,不存储局部变量。

字段表

  • fields 表中的每个成员都必须是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述。

字段表访问标识

我们知道,一个字段可以被各种关键字去修饰,比如:作用域(public、private、protected 修饰符)、是实例变量还是类变量(static 修饰符)、可变性(final)、并发可见性(volatile 修饰符,是否强制从主内存读写)、可否序列化(transient 修饰符)、字段数据类型(基本数据类型、对象、数组)等等。因此,其可像类的访问标志那样,使用一些标志来标记字段。字段的访问标志有如下:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段是否为 public
ACC_PRIVATE 0x0002 字段是否为 private
ACC_PROTECTED 0x0004 字段是否为 protected
ACC_STATIC 0x0008 字段是否为 static
ACC_FINAL 0x0010 字段是否为 final
ACC_VOLATILE 0x0040 字段是否为 volatile
ACC_TRANSTENT 0x0080 字段是否为 transient
ACC_SYNCHETIC 0x1000 字段是否为由编译器自动产生
ACC_ENUM 0x4000 字段是否为 enum

字段表结构:

  • 根据字段名索引的值,查询常量池中的指定索引项即可。

  • 描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte,char,double,float,int,1ong,short,boolean)及代表无返回值的 void 类型都用一个大写字符来表示,而对象则用字符 L 加对象的全限定名来表示,可参考常量池章节。

  • 一个字段还可能拥有一些属性,用于存储更多的额外信息。比如初始化值、一些注释信息等。属性个数存放在 attribute_count 中,属性具体内容存放在 attributes 数组中。

    类型 名称 数量 含义
    u2 access_flags 1 访问标志
    u2 name_index 1 字段名索引
    u2 descriptor_index 1 描述符索引
    u2 attributes_count 1 属性计数器
    attribute_info attributess attributes_count 属性集合

属性表集合

以常量属性为例,结构为:

1
2
3
4
5
6
// 例如常量的属性表结构如下
ConstantValue_attribute{
u2 attribute_name_index; // 属性名索引
u4 attribute_length; // 属性长度,对于常量而言,attribute_length值恒为2
u2 constantvalue_index; // 属性值
}

方法表计数器

字段表后边紧跟的是 方法表计数器,方法表计数器后边紧跟的是 方法表

  • 方法表计数器(methods_count):记录方法表中字段的数量,为无符号数类型 u2 大小。

  • 方法表(methods):方法表是一个表结构的类型数据 method_info, 存储了当前类或者当前接口中的 public 方法,protected 方法,default 方法,private 方法等。

    • methods 只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。另一方面,methods 表有可能会出现由编译器自动添加的方法
    • 在字节码文件中,每一个 method_info 项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符(public、private 或 protected),方法的返回值类型以及方法的参数信息等。
    • 如果这个方法不是抽象的或者不是 native 的,那么字节码中会体现出来, 最典型的便是编译器产生的方法信息(比如:类(接口)初始化方法(cinit)和实例初始化方法(init))。

使用注意事项: 返回值不一样的方法(同名同参数),java 不允许。但 class 文件相反,只要求方法的返回值不能相同

  • 在 Java 语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。
  • 但在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个 class 文件中。
  • 也就是说,尽管 Java 语法规范并不允许在一个类或者接口中声明多个方法签名相同的方法,但是和 Java 语法规范相反,字节码文件中却恰恰允许存放多个方法签名相同的方法,唯一的条件就是这些方法之间的返回值不能相同。

方法表

方法表访问标志: 跟字段表一样,方法表也有访问标志,而且他们的标志有部分相同,部分则不同,方法表的具体访问标志如下:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段是否为 public
ACC_PRIVATE 0x0002 字段是否为 private
ACC_PROTECTED 0x0004 字段是否为 protected
ACC_STATIC 0x0008 字段是否为 static
ACC_FINAL 0x0010 字段是否为 final
ACC_VOLATILE 0x0040 字段是否为 volatile
ACC_TRANSTENT 0x0080 字段是否为 transient
ACC_SYNCHETIC 0x1000 字段是否为由编译器自动产生
ACC_ENUM 0x4000 字段是否为 enum
  • methods 表中的每个成员都必须是一个 method_info 结构,用于表示当前类或接口中某个方法的完整描述。如果某个 method_info 结构的 access_flags 项既没有设置 ACC_NATIVE 标志也没有设置 ACC_ABSTRACT 标志,那么该结构中也应包含实现这个方法所用的 Java 虚拟机指令。

  • method_info 结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法和类或接口初始化方法

  • Java 程序方法体中的代码经过 javac 编译处理后,最终变为字节码指令存在 Code 属性中,Code 属性出现在方法表的属性集合之中,但并非所有的方法表都有 code 属性,例如抽象类或接口

  • 方法表的结构实际跟字段表是一样的,方法表结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // 方法表
    method_info {
    u2 access_flags; // 访问标志
    u2 name_index; // 方法名索引
    u2 descriptor_index; // 描述符索引
    u2 attributes_count; // 属性计数器
    attribute_info attributes[attributes_count]; // 属性集合
    }



    // Code_attribute_info 结构如下:
    Code_attribute {
    u2 attribute_name_index; // 属性名称 ,常量固定值为 "Code"
    u4 attribute_length; //属性实际长度
    u2 max_stack; // 操作数栈深的最大值,方法的栈深
    u2 max_locals; // 局部变量所表示的存储空间 单位Slot
    u4 code_length; // 代码行数
    u1 code[code_length]; // 存储java源代码编译后产生的字节码执行
    u2 exception_table_length; // 异常表长度
    { u2 start_pc;
    u2 end_pc;
    u2 handler_pc;
    u2 catch_type;
    }
    exception_table[exception_table_length]; // 异常表信息
    u2 attributes_count; // 属性表数
    attribute_info attributes[attributes_count]; // 属性表
    }

属性表集合

  • 方法表集合之后的属性表集合,指的是 class 文件所携带的辅助信息,比如该 class 文件的源文件的名称。以及任何带有 RetentionPolicy.CLASS 或者 RetentionPolicy.RUNTIME 的注解。这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试,一般无须深入了解。
  • 此外,字段表、方法表都可以有自己的属性表。用于描述某些场景专有的信息。
  • 属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但 Java 虚拟机运行时会忽略掉它不认识的属性。

attributes [](属性表)

  • 属性表的每个项的值必须是 attribute_info 结构。属性表的结构比较灵活,各种不同的属性只要满足以下结构即可。

  • 属性的通用格式:即只需说明属性的名称以及占用位数的长度即可,属性表具体的结构可以去自定义。

    1
    2
    3
    4
    5
    6
    // 属性表 attribute_info 是最复杂的结构,其中就包含指令集 Code 属性。
    attribute_info {
    u2 attribute_name_index; // 属性名称,指向常量池
    u4 attribute_length; //属性实际长度,不包含 attribute_name_index 和 attribute_length 的长度,也就是说实际长度 = 2 + 4 + attribute_length
    u1 info[attribute_length]; //属性实际的信息
    }
  • 属性表实际上可以有很多类型,上面看到的 Code 属性只是其中一种,Java8 里面定义了 23 种属性。可看官网: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7

    属性名称 使用位置 含义
    1 Code 方法表 Java 代码编译成的字节码定义
    2 ConstantValue 字段表 final 关键字定义的常量池
    3 Deprecated 类,方法,字段表 被声明为 deprecated 的方法和字段
    4 Exceptions 方法表 方法抛出的异常
    5 EnclosingMethod 类文件 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法
    6 InnerClass 类文件 内部类列表
    7 LineNumberTable Code 属性 Java 源码的行号于字节码指令的对应关系
    8 LocalVaribaleTable Code 属性 方法的局部变量描述
    9 StackMapTable Code 属性 JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配
    10 Signature 类,方法表,字段表 用于支持泛型情况下的方法签名
    11 SourceFile 类文件 记录源文件名称
    12 SourceDebugException 类文件 用于存储额外的调试信息
    13 Synthetic 类,方法表,字段表 标志方法或字段为编译器自动生成的
    14 LocalVaribaleTypeTable 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
    15 RuntimeVisibleAnnotations 类,方法表,字段表 为动态注解提供支持
    16 RuntimeInvisibleAnnotations 表,方法表,字段表 用于指明哪些注解是运行时不可见的
    17 RuntimeVisibleParameterAnnotation 方法表 作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法
    18 RuntimeInvisibleParameterAnnotation 方法表 作用与 RuntimeInvisibleAnnotations 属性类似,作用对象哪个为方法参数
    19 AnnotationDefault 方法表 用于记录注解类元素的默认值
    20 BootsrapMethods 类文件 用于保存 invokeddynamic 指令引用的引导方式限定符

部分属性详解

Code 属性

Code 属性就是存放 javac 编译器编译处理后的 字节码指令,一个字节表示一个指令。但是,并非所有方法表都有 Code 属性。像接口或者抽象方法,他们没有具体的方法体,因此也就不会有 Code 属性了。

Code 属性表的结构,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Code_attribute {
u2 attribute_name_index; //属性名索引
u4 attribute_length; //属性长度
u2 max_stack; //操作数栈深度的最大值
u2 max_locals; //局部变量表所需的存续空间
u4 code_length; //字节码指令的长度
u1 code[code_length]; //存储字节码指令
u2 exception_table_length; //异常表长度
{ u2 start_pc; //起始字节码(try作用域起点)
u2 end_pc; //终止字节码(try作用域终点)
u2 handler_pc; //异常处理字节码的起始位置
u2 catch_type; //catch的异常类型
// 表示当start_pc到end_pc间的字节码执行抛出catch_type类型的异常时,使用handler_pc处的代码处理
}
exception_table[exception_table_length]; //异常表
u2 attributes_count; //属性集合计数器
attribute_info attributes[attributes_count];//属性集合
}

可以看到:Code 属性表的前两项跟属性表是一致的,即 Code 属性表遵循属性表的结构,后面那些则是他自定义的结构。

LineNumberTable 属性

LineNumberTable 属性是可选变长属性,位于 Code 结构的属性表。
LineNumberTable 属性是 用来描述Java源码行号与字节码行号之间的对应关系。 这个属性可以用来在调试的时候定位代码执行的行数。可在编译时加 -g: none 或 -g: lines 参数关闭。

  • start_pc,即字节码行号;line_number,即Java源代码行号。

  • 在 Code 属性的属性表中,LineNumberTable 属性可以按照任意顺序出现,此外,多个 LineNumberTable 属性可以共同表示一个行号在源文件中表示的内容,即 LineNumberTable 属性不需要与源文件的行一一对应。

  • LineNumberTable 属性表结构:

1
2
3
4
5
6
7
8
9
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{
u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length]
}

LocalVariableTable 属性

LocalVariableTable 是可选变长属性,位于 Code 属性的属性表中, 描述栈帧中局部变量表与 Java 源码中定义的变量之间的关系,它被调试器用于确定方法在执行过程中局部变量的信息。可在编译时加 -g: none 或 -g: vars 参数关闭。

在 Code 属性的属性表中,LocalVariableTable 属性可以按照任意顺序出现。Code 属性中的每个局部变量最多只能有一个 LocalVariableTable 属性。

  • start pc + length 表示这个变量在字节码中的生命周期起始和结束的偏移位置(this生命周期从头0到结尾10)

  • index就是这个变量在局部变量表中的槽位(槽位可复用)

  • name就是变量名称

  • Descriptor表示局部变量类型描述

  • LocalVariableTable 属性表结构:

1
2
3
4
5
6
7
8
9
10
11
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_variable_table_length;
{
u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
} local_variable_table[line_variable_table_length];
}

Exceptions 属性

与 Code 属性平级的属性,不是 exception_table !

1
2
3
4
5
6
Exceptions_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_exceptions; // 可能抛出的异常数
u2 exception_index_table[number_of_exceptions]; // 异常名称,指向常量池CONSTATNT_UTF8_INFO类型的元素
}

类的附加属性 SourceFile

1
2
3
4
5
SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index; // 编译此类文件的源文件的名称索引
}

Class 文件结构的小结

  • 随着 Java 平台的不断发展,在将来,Class 文件的内容也一定会做进一步的扩充,但是其基本的格式和结构不会做重大调整。
  • 从 Java 虚拟机的角度看,通过 Class 文件,可以让更多的计算机语言支持 Java 虚拟机平台。因此,Class 文件结构不仅仅是 Java 虚拟机的执行入口,更是 Java 生态圈的基础和核心。