JVM-字节码文件格式

57

JVM概述

1、JVM的生命周期

  • 启动

    ​ Java虚拟机的启动时通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由Java虚拟机具体实现指定的。

  • 执行

    ​ 真正执行程序的是一个叫Java的虚拟机进程。

  • 退出

    ​ 虚拟机退出有以下几种情况:

    ​ 1、程序正常退出

    ​ 2、程序运行过程中遇到了异常或者错误而终止

    ​ 3、由于操作系统出错造成Java虚拟机进程终止

    ​ 4、某线程中调用Runtime类或者System类的exit方法(最终都是调用halt0本地方法),或Runtime类的halt方法,且Java安全 管理器也允许本次的exit或者halt操作

2、说说JVM的代表HotSpot

  1. SUN于JDK1.3.1版本开始使用HotSpot虚拟机,2006年开源,主要采用C++实现,JNI接口部分采用C实现。
  2. HotSpot采用JIT(Just In Time)编译器,把常用的热点代码编译成本地代码(机器码),大大提高了Java程序的运行性能。
  3. HotSpot的JVM参数分为规则参数(Standard Options)和非规则参数(Non-Standard Options),规则参数相对稳定,在后续的版本中一般不会有大的变动,而非规则参数则不一定。

3、JVM的简要架构图

image-20230725154158275

字节码的概述

1、字节码文件是什么?

源码经过编译后生成二进制类文件,它的内容是JVM的指令。

2、编译器的类型

​ 编译器分为前端编译器和后端编译器,前端编译器是将源码编译为符合JVM规范的二进制类文件,而后端编译器则是在后续的即时编译阶段把字节码转换为对应运行平台的机器码。

​ 前端编译器并不会直接涉及编译优化的技术,而是将具体的优化细节交给后端编译器(JIT编译器)来负责。

3、生成class文件的编译器

​ 前端编译器的任务就是将源码编译为字节码文件,我们通常使用的就是javac这个前端编译器,当然并不只局限这一个,任何能把源码编译成符合JVM规范的字节码文件的编译器均可,例如Eclipse的ECJ(Eclipse Compiler for Java)。相对于javac, javac是全量编译,而ECJ是增量编译,所以编译速度会更快。

4、AOT

​ JDK9引入了AOT编译器(静态提前编译器:Ahead of Time Compiler),它的编译工具是jaotc,借助graal编译器,直接把类文件源码转为机器码,并存放至动态共享库之中。

​ AOT编译和JIT是一个相对的概念,JIT是在运行的过程中,将热点代码编译为对应平台的机器码,并部署到环境中的过程。而AOT则是在程序运行之前直接把字节码转换为对应平台的机器码。

AOT对比JIT的优缺点:

​ 优点:运行前就编译成了机器码,无需再遇热,减轻首次访问应用‘慢’的感觉。

​ 缺点:1、针对不同平台需要编译多次,失去了Java最初宣传的‘Compiler once, Run Anywhere’的目的。2、降低了Java的链接过程的动态性,加载的代码在编译器就必须全部已知。

5、哪些类型有Class对象?

  1. class: 外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
  2. interface
  3. [] :数组,数组的class就是它内部元素的class
  4. enum
  5. annotation
  6. primitive types:基本数据类型
  7. void

6、字节码指令

​ Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。

aload_0
bipush 40

细看Class文件的内容

1、概述

  • 魔数:cafebabe
  • Class文件版本(minor_version(小版本), major_version(大版本))
  • 常量池
  • 访问标识符
  • 类索引,父类索引,接口索引
  • 字段表
  • 方法表
  • 属性表

如下所示,为oracle官网虚拟机规范提到的字节码文件的结构:

Chapter 4. The class File Format (oracle.com)

ClassFile {
    u4             magic; // 魔数CAFEBABE
    u2             minor_version; // Class文件的小版本号
    u2             major_version; // Class文件的大版本号 https://en.wikipedia.org/wiki/Java_version_history
    u2             constant_pool_count; // 常量池中的数量
    cp_info        constant_pool[constant_pool_count-1]; // 常量池表
    u2             access_flags; // 访问修饰符
    u2             this_class; // 当前类的索引
    u2             super_class; // 父类索引
    u2             interfaces_count; // 接口数量
    u2             interfaces[interfaces_count]; // 接口索引
    u2             fields_count; // 字段个数
    field_info     fields[fields_count]; // 字段索引 
    u2             methods_count; // 方法数量
    method_info    methods[methods_count]; // 方法索引
    u2             attributes_count; // 属性个数
    attribute_info attributes[attributes_count]; // 属性索引
}

2、魔数

魔数是Class文件的标识符,用来确定字节码文件是否为一个可以被JVM接受的有效合法的Class文件。其固定值就是一个4字节的0XCAFEBABE。

3、字节码文件版本号

  • 紧接着魔数的 4 个字节存储的是 Class 文件的版本号。同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本号minor_version,而第7个和第8个字节就是编译的主版本号major_version。

  • 它们共同构成了class文件的格式版本号。譬如某个 Class 文件的主版本号为 M,副版本号为 m,那么这个Class 文件的格式版本号就确定为 M.m。

image-20230726094310305

4、class常量池

各种字符串常量、类和接口名称、字段名称以及 ClassFile 结构及其子结构中引用的其它常量(也就是字面量和符号引用)。

image-20230726153850437

对我来说上图中第一眼不理解的概念就是描述符,下面具体说说描述符是什么?

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

image-20230726161059797

​ 用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如:

​ 方法double add(double[] x, double y)的描述符为([DD) D。

​ 方法byte[] readClass2Byte(String name)的描述符为(Ljava/lang/String;)[B

符号引用和直接引用的区别和关联:

  • 符号引用是以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定加载到了内存当中。

  • 直接引用是可以直接指向目标的指针,偏移量或能够间接定位到目标的句柄。直接引用与虚拟机实现的内存布局相关。相同的符号引用在不同的虚拟机上翻译出的直接引用一般不会相同。如果有了直接引用,说明引用的目标一定存在于内存之中。


​ 常量池表以 1 ~ constant_pool_count - 1为索引。表明了后面有多少个常量项。

​ 为什么索引不是从0开始的呢,因为把第0项常量空出来了。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。

​ 常量池中每一项常量都是一个表,表结构起始的第一位是个u1类型的标志位(tag,取值见下表中标志列),代表着当前常量属于哪种常量类型,常量类型各自有着完全独立的数据结构,具体参照JVM规范内的定义:Chapter 4. The class File Format (oracle.com)

image-20230726152325518

常量类型和结构

常量池中每一项常量都是一个表,JDK1.7之后共有14种不同的表结构数据。如下表格所示:

image-20230726164331201

总结1:

  • 这14种表(或者常量项结构)的共同点是:表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。

  • 在常量池列表中,CONSTANT_Utf8_info常量项是一种使用改进过的UTF-8编码格式(UTF-8缩略编码与普通UTF-8编码的区别是:

    从'\u0001'到'\u007f'之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,

    从'\u0080'到'\u07ff'之间的所有字符的缩略编码用两个字节表示,

    从'\u0800'开始到'\uffff'之间的所有字符 的缩略编码就按照普通UTF-8编码规则使用三个字节表示。)来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息。

  • 这14种常量项结构还有一个特点是,其中13个常量项占用的字节固定,只有CONSTANT_Utf8_info占用字节不固定,其大小由length决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,大小不固定,编译后,通过utf-8缩略编码,就可以知道其长度。

5、访问标识符

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

image-20230726165259913
  • 每一种类型的表示都是通过设置访问标记的32位中的特定位(字节码里)来实现的。比如,若是public final的类,则该标记为ACC_PUBLIC | ACC_FINAL。
  • 使用ACC_SUPER可以让类更准确地定位到父类的方法super.method(),现代编译器都会设置并且使用这个标记。

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

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

image-20230726170925360

this_class(类索引)

​ 2字节无符号整数,指向常量池的索引。它提供了类的全限定名,如cc/xuhao/tp/Test。this_class的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为CONSTANT_Class_info类型结构体,该结构体表示这个class文件所定义的类或接口。

super_class (父类索引)

  • 2字节无符号整数,指向常量池的索引。它提供了当前类的父类的全限定名。如果我们没有继承任何类,其默认继承的是java/lang/Object类。同时,由于Java不支持多继承,所以其父类只有一个。

  • superclass指向的父类不能是final。

interfaces

  • 指向常量池索引集合,它提供了一个符号引用到所有已实现的接口

  • 由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的CONSTANT_Class (当然这里就必须是接口,而不是类)。

interfaces_count (接口计数器)

​ interfaces_count项的值表示当前类或接口的直接超接口数量。

interfaces[] (接口索引集合)

​ interfaces []中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为 interfaces_count。 每个成员 interfaces[i]必须为 CONSTANT_Class_info结构,其中 0 <= i < interfaces_count。在 interfaces[]中,各成员所表示的接口顺序和对应的源代码中给定的 接口顺序(从左至右)一样,即 interfaces[0]对应的是源代码中最左边的接口。

7、字段表

fields_count (字段计数器)

​ fields_count的值表示当前class文件fields表的成员个数。使用2个字节来表示。

​ fields表中每个成员都是一个field_info结构,用于表示该类或接口所声明的所有类字段或者实例字段,不包括方法内部声明的变量,也不包括从父类或父接口继承的那些字段。

6.2 fields[](字段表)

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

  • 一个字段的信息包括如下这些信息。这些信息中,各个修饰符都是布尔值,要么有,要么没有。

​ > 作用域(public、private、protected修饰符)

​ > 是实例变量还是类变量(static修饰符)

​ > 可变性(final)

​ > 并发可见性(volatile修饰符,是否强制从主内存读写)

​ > 可否序列化(transient修饰符)

​ > 字段数据类型(基本数据类型、对象、数组)

​ > 字段名称

field_info的结构:

image-20230726180343037

⚠️注意事项:

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

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

8、方法表

methods:指向常量池索引集合,它完整描述了每个方法的签名。

  • 在字节码文件中,每一个method_info项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符(public、private或protected),方法的返回值类型以及方法的参数信息等。

  • 如果这个方法不是抽象的或者不是native的,那么字节码中会体现出来。

  • 一方面,methods表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。另一方面,methods表有可能会出现由编译器自动添加的方法,最典型的便是编译器产生的方法信息(比如:类(接口)初始化方法()和实例初始化方法())。

使用注意事项:

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

​ 也就是说,尽管Java语法规范并不允许在一个类或者接口中声明多个方法签名相同的方法,但是和Java语法规范相反,字节码文件中却恰恰允许存放多个方法签名相同的方法,唯一的条件就是这些方法之间的返回值不能相同。

method[]

  • method[]中的每个成员都是method_info结构,是当前类或者接口的某个方法的完整描述。如果某个method_info的access_flags没有被设置为ACC_ABSTRACT或者ACC_NATIVE,那么这个method_info中应当包含这个方法的JVM指令。
  • method_info可以表示类或者接口中定义的所有方法。包括实例方法,类方法(静态方法),实例初始方法,类或接口的初始方法。

method_info结构

image-20230727094813012

9、属性表

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

​ 与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一 些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

attribute_info[]的内容:

  • ConstantValue

​ 它的作用是通知虚拟机自动为静态变量赋值。《Java虚拟机规范》要求只有被static关键字修饰的变量(类变量)才可以使用这项属性。但我们常用的编译器javac则是还添加了变量(这样也就是常量了)必须是final关键字修饰且变量是基本类型或者String的才可能够使用ContantValue来初始化。

结构:

image-20230727142744989
  • Deprecated

​ 布尔类型属性值,为了支持**注释(注意是注释,不是方法等上面的注解)**中的关键词@deprecated 而引入的。

结构:

image-20230727153537034
  • Code

​ 存储方法体中的代码转换成的JVM可识别的字节码指令。接口方法(default应该不算)和抽象方法是没有方法体的,所以Code属性自然是没有的。

结构:

image-20230727155200488
  • Exceptions

​ 和上面的Code里的异常表不是一个东西,它和Code同一级。反映的是方法后throws的Checked Exceptions的信息。

结构:

image-20230727160216809
  • InnerClasses

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

InnerClasses结构:

image-20230727162128444

inner_classes_info结构:

image-20230727162305847
  • LineNumberTable

    ​ 描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。不是运行时必须属性。在调试代码是用来定位代码执行的行数。位于Code的属性表中。

结构:

image-20230727163335567

​ line_number_info包含start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。

  • LocalVariableTable

​ LocalVariableTable 是可选变长属性,位于 Code属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息。在 Code 属性的属性表中,LocalVariableTable 可以按照任意顺序出现。 Code 属性中的每个局部变量最多只能有一个 LocalVariableTable 属性。

LocalVariableTable结构:

image-20230727163931987

local_variable_info的结构:

image-20230727164205325
	1、start_pc + length表示这个变量在字节码中的生命周期起始和结束的偏移位置

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

​ 3、name_index就是变量名称在常量池中的索引

​ 4、descriptor_index表示局部变量类型描述在常量池中的索引

  • Signature

    ​ Signature属性在JDK 5增加到Class文件规范之中,它是一个可选的定长属性,可以出现于类、字段表和方法表结构的属性表中。任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variable)或参数化类型(Parameterized Type),则Signature属性会为它记录泛型签名信息。

    ​ Java实现的范型是采用擦除法实现的伪范型,字节码(Code属性)中所有范型信息编译(类型变量,参数化变量)在编译后都会被擦除。

    结构:

    image-20230727165845248
  • SourceFile

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

结构:

image-20230727170049949

and so on.......