JVM - 2 JVM 结构

55 阅读41分钟

image.png

JVM - 2 JVM 结构

为什么要掌握 JVM 相关知识?

  1. 面向工资学习,为雇主提供更有价值的技能,换取更高额的工资回报;
  2. 写出更加精确的代码,就像人的行为的背后是心理、价值观等决定的, java 代码就是那个行为, 对 JVM 有更深入的认识后才能写出更精准的代码;
  3. just like , 别问那么多就是爱好技术, 兴趣是最好的老师;

Data Types 数据类型

  • primitive types 基本数据类型
    • byte 值为 8 位有符号二进制补码整数,其默认值为零
    • short 值为 16 位有符号二进制补码整数,默认值为零
    • int 值为 32 位有符号二进制补码整数,默认值为零
    • long 值为 62 位有符号二进制补码整数,默认值为零
    • char 值是 16 位无符号整数,表示 Basic Multilingual Plane 中的 Unicode 代码点,使用 UTF-16 编码,其默认值为空代码点 ('u0000').
    • float 值为浮点值集或支持的浮点扩展指数值集的元素,其默认值为正零
    • double 值是双精度值集的元素,或者在支持的情况下,是双精度扩展指数值集的元素,并且其默认值为正零
    • boolean 布尔类型的值编码真值 true 和 false,默认值为 false。
    • returnAddress 类型的值是指向 Java 虚拟机指令操作码的指针。在原始类型中,只有 returnAddress 类型不直接与 Java 编程语言类型相关联。returnAddress 类型由 Java 虚拟机的 jsr、ret 和 jsr_w 指令(§jsr、§ret、§jsr_w)使用。 returnAddress 类型的值是指向 Java 虚拟机指令操作码的指针。与数字原始类型不同,returnAddress 类型不对应于任何 Java 编程语言类型,并且不能被正在运行的程序修改。

尽管 Java 虚拟机定义了一个 boolean 类型,但它只提供了非常有限的支持。没有专门用于布尔值操作的 Java 虚拟机指令。相反,Java 编程语言中对布尔值进行运算的表达式被编译为使用 Java 虚拟机 int 数据类型的值。Java 虚拟机确实直接支持布尔数组。它的 newarray 指令 (§newarray) 支持创建布尔数组。使用字节数组指令 baload 和 bastore (§baload, §bastore) 访问和修改布尔类型的数组。在 Oracle 的 Java 虚拟机实现中,Java 编程语言中的布尔数组被编码为 Java 虚拟机字节数组,每个布尔元素使用 8 位。Java 虚拟机使用 1 表示 true 和 0 表示 false 对布尔数组组件进行编码。编译器将 Java 编程语言布尔值映射到 Java 虚拟机类型 int 的值时,编译器必须使用相同的编码。

  • reference types 引用数据类型
    • class 类类型
    • array 数组类型
    • interface 接口类型

引用值也可以是特殊的空引用,即没有对象的引用,这里用null表示。空引用最初没有运行时类型,但可以转换为任何类型。引用类型的默认值为 null。

Run-Time Data Areas 运行时数据区

Java 虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区域是在 Java 虚拟机启动时创建的,只有在 Java 虚拟机退出时才被销毁。其他数据区域是每个线程。每线程数据区在创建线程时创建,并在线程退出时销毁。

pc Register 程序计数器

Java 虚拟机可以同时支持多个执行线程(JLS §17)。每个 Java 虚拟机线程都有自己的 pc(程序计数器)寄存器。在任何时候,每个 Java 虚拟机线程都在执行单个方法的代码,即该线程的当前方法 (§2.6)。如果该方法不是本机方法,则 pc 寄存器包含当前正在执行的 Java 虚拟机指令的地址。如果线程当前正在执行的方法是native,那么Java虚拟机的pc寄存器的值是未定义的。 Java 虚拟机的 pc 寄存器足够宽,可以容纳 returnAddress 或特定平台上的本机指针。

Java Virtual Machine Stacks , Java 虚拟机栈

每个 Java 虚拟机线程都有一个私有的 Java 虚拟机堆栈,与线程同时创建。 Java 虚拟机堆栈存储帧(§2.6)。 Java 虚拟机堆栈类似于 C 等常规语言的堆栈:它保存局部变量和部分结果,并在方法调用和返回中发挥作用。因为 Java 虚拟机堆栈从不直接操作,除了压入和弹出帧外,帧可能是堆分配的。 Java 虚拟机堆栈的内存不需要是连续的。

如果线程中的计算需要比允许的更大的 Java 虚拟机堆栈,Java 虚拟机将抛出 StackOverflowError。

如果 Java 虚拟机堆栈可以动态扩展,并且尝试扩展但没有足够的内存可用于实现扩展,或者如果没有足够的内存可用于为新线程创建初始 Java 虚拟机堆栈,则 Java 虚拟机机器抛出 OutOfMemoryError。

Heap 堆

Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的堆。堆是运行时数据区域,从中分配所有类实例和数组的内存。堆是在虚拟机启动时创建的。对象的堆存储由自动存储管理系统(称为垃圾收集器)回收;对象永远不会显式释放。 Java 虚拟机没有假定特定类型的自动存储管理系统,可以根据实现者的系统要求选择存储管理技术。堆可以是固定大小的,也可以根据计算的需要进行扩展,如果不需要更大的堆,则可以收缩。堆的内存不需要是连续的。

如果计算需要的堆多于自动存储管理系统所能提供的堆,则 Java 虚拟机将抛出 OutOfMemoryError。

Method Area 方法区

Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区。方法区类似于常规语言的编译代码的存储区,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化以及接口初始化中使用的特殊方法(§2.9)。 方法区是在虚拟机启动时创建的。尽管方法区在逻辑上是堆的一部分,但简单的实现可能会选择不对其进行垃圾收集或压缩。本规范不强制要求方法区的位置或用于管理已编译代码的策略。方法区的大小可以是固定的,也可以根据计算的需要进行扩展,如果不需要更大的方法区,则可以缩小。方法区的内存不需要是连续的。

Run-Time Constant Pool 运行时常量池

运行时常量池是类文件中 constant_pool 表的按类或按接口的运行时表示(第 4.4 节)。它包含多种常量,从编译时已知的数字文字到必须在运行时解析的方法和字段引用。运行时常量池的功能类似于传统编程语言的符号表,尽管它包含的数据范围比典型的符号表更广泛。每个运行时常量池都是从 Java 虚拟机的方法区(§2.5.4)分配的。类或接口的运行时常量池是在 Java 虚拟机创建类或接口(第 5.3 节)时构建的。

在创建类或接口时,如果构建运行时常量池需要的内存多于 Java 虚拟机方法区可用的内存,则 Java 虚拟机将抛出 OutOfMemoryError。

Native Method Stacks 本地方法栈

Java 虚拟机的实现可以使用传统的堆栈,通俗地称为“C 堆栈”,以支持本地方法(用 Java 编程语言以外的语言编写的方法)。 Java 虚拟机指令集的解释器的实现也可以使用本地方法堆栈,这些语言使用诸如 C 的语言。无法加载本地方法并且本身不依赖于传统堆栈的 Java 虚拟机实现不需要提供本地方法堆栈。如果提供,本地方法堆栈通常在创建每个线程时按线程分配。


Frames 栈帧

栈帧用于存储数据和部分结果,以及执行动态链接、方法返回值和分派异常。 每次调用方法时都会创建一个新栈帧。栈帧在其方法调用完成时被销毁,无论该完成是正常的还是突然的(它抛出未捕获的异常)。帧是从创建帧的线程的 Java 虚拟机堆栈 (§2.5.2) 分配的。每个帧都有自己的局部变量数组(第 2.6.1 节)、自己的操作数堆栈(第 2.6.2 节)和对当前方法类的运行时常量池(第 2.5.5 节)的引用.可以使用附加的特定于实现的信息(例如调试信息)来扩展帧。局部变量数组和操作数堆栈的大小在编译时确定,并随与帧关联的方法的代码一起提供(第 4.7.3 节)。因此,帧数据结构的大小仅取决于 Java 虚拟机的实现,并且这些结构的内存可以在方法调用时同时分配。

在给定的控制线程中的任何时候,只有一个栈帧(执行方法的框架)处于活动状态。此帧称为当前帧,其方法称为当前方法。定义当前方法的类是当前类。对局部变量和操作数栈的操作通常参考当前帧。

如果一个栈帧的方法调用了另一个方法或者它的方法完成了,那么这个栈帧就不再是当前的。调用方法时,将创建一个新栈帧,并在控制权转移到新方法时成为当前栈帧。在方法返回时,当前帧将其方法调用的结果(如果有)传回给前一帧。当前一帧成为当前帧时,当前帧将被丢弃。请注意,一个线程创建的帧是该线程的本地帧,不能被任何其他线程引用。

Local Variables 本地变量表

每个帧(第 2.6 节)都包含一个称为局部变量的变量数组。帧的局部变量数组的长度在编译时确定,并以类或接口的二进制表示形式连同与帧关联的方法的代码一起提供(第 4.7.3 节)。

单个局部变量可以保存类型为 boolean、byte、char、short、int、float、reference 或 returnAddress 的值。一对局部变量可以保存 long 或 double 类型的值。

局部变量通过索引寻址。第一个局部变量的索引为零。当且仅当该整数介于零和比局部变量数组的大小小一之间时,该整数才被认为是局部变量数组的索引。

long 或 double 类型的值占用两个连续的局部变量。这样的值只能使用较小的索引来寻址。例如,在索引为 n 的局部变量数组中存储的一个 double 类型的值实际上占用了索引为 n 和 n+1 的局部变量;但是,无法从索引 n+1 处加载局部变量。可以存入。但是,这样做会使局部变量 n 的内容无效。

Java 虚拟机不要求 n 是偶数。直观地说,long 和 double 类型的值不需要在局部变量数组中进行 64 位对齐。实现者可以自由决定使用为值保留的两个局部变量来表示此类值的适当方式。

Java 虚拟机使用局部变量在方法调用时传递参数。在类方法调用中,任何参数都从局部变量 0 开始传递到连续的局部变量中。在实例方法调用中,局部变量 0 始终用于传递对调用实例方法的对象的引用(Java 中的 this编程语言)。从局部变量 1 开始,随后将任何参数传递到连续的局部变量中。

Operand Stacks 操作数栈

每个帧 (§2.6) 都包含一个后进先出 (LIFO) 堆栈,称为操作数堆栈。帧的操作数堆栈的最大深度在编译时确定,并随与帧关联的方法的代码一起提供(第 4.7.3 节)。在上下文清楚的情况下,我们有时会将当前帧的操作数堆栈简称为操作数堆栈。

创建包含它的帧时,操作数堆栈为空。 Java 虚拟机提供了将常量或值从局部变量或字段加载到操作数堆栈的指令。其他 Java 虚拟机指令从操作数栈中获取操作数,对其进行运算,然后将结果推回操作数栈。操作数栈还用于准备传递给方法的参数和接收方法结果。

例如,iadd 指令 (§iadd) 将两个 int 值相加。它要求要添加的 int 值是操作数堆栈的顶部两个值,由先前的指令推送到那里。两个 int 值都从操作数堆栈中弹出。它们被相加,它们的和被推回操作数栈。子计算可以嵌套在操作数堆栈上,从而产生可由包含计算使用的值。

操作数栈上的每个条目都可以保存任何 Java 虚拟机类型的值,包括 long 类型或 double 类型的值。

来自操作数堆栈的值必须以适合其类型的方式进行操作。例如,不可能压入两个 int 值并随后将它们视为 long 或压入两个 float 值并随后使用 iadd 指令将它们相加。少量 Java 虚拟机指令(dup 指令 (§dup) 和 swap (§swap))作为原始值在运行时数据区域上运行,而不考虑它们的特定类型;这些指令的定义方式使其不能用于修改或分解单个值。这些对操作数堆栈操作的限制是通过类文件验证(§4.10)强制执行的。

在任何时间点,操作数堆栈都有关联的深度,其中 long 或 double 类型的值贡献两个单位的深度,任何其他类型的值贡献一个单位。

Dynamic Linking 动态链接

每个帧 (§2.6) 都包含对当前方法类型的运行时常量池 (§2.5.5) 的引用,以支持方法代码的动态链接。方法的类文件代码是指通过符号引用调用的方法和访问的变量。动态链接将这些符号方法引用转换为具体方法引用,加载类以解析尚未定义的符号,并将变量访问转换为与这些变量的运行时位置相关的存储结构中的适当偏移量。这种方法和变量的后期绑定使得方法使用的其他类中的更改不太可能破坏此代码。

Normal Method Invocation Completion 正常方法调用完成

如果方法调用没有导致异常(§2.10)被抛出,则方法调用正常完成,无论是直接从 Java 虚拟机还是作为执行显式 throw 语句的结果。如果当前方法的调用正常完成,则可以向调用方法返回一个值。当调用的方法执行返回指令之一(第 2.11.8 节)时会发生这种情况,返回指令的选择必须适合返回值的类型(如果有)。

当前帧(§2.6)在这种情况下用于恢复调用者的状态,包括其局部变量和操作数堆栈,调用者的程序计数器适当递增以跳过方法调用指令。然后在调用方法的帧中正常继续执行,并将返回值(如果有)压入该帧的操作数堆栈。

Abrupt Method Invocation Completion 方法调用突然完成

如果在方法内执行 Java 虚拟机指令导致 Java 虚拟机抛出异常(第 2.10 节),并且该异常未在方法内处理,则方法调用突然完成。执行 athrow 指令 (§athrow) 也会导致显式抛出异常,如果当前方法未捕获异常,则会导致方法调用突然完成。突然完成的方法调用永远不会向其调用者返回值。


Representation of Objects

Java 虚拟机不要求对象有任何特定的内部结构。

在 Oracle 的一些 Java 虚拟机实现中,对类实例的引用是指向句柄的指针,该句柄本身是一对指针:一个指向包含对象方法的表,一个指向代表类对象的类对象对象的类型,另一个是从堆中为对象数据分配的内存。

Floating-Point Arithmetic 浮点运算

Java 虚拟机合并了 IEEE 二进制浮点算法标准(ANSI/IEEE 标准 754-1985,纽约)中指定的浮点算法的子集。

Java Virtual Machine Floating-Point Arithmetic and IEEE 754

Java 虚拟机支持的浮点运算与 IEEE 754 标准之间的主要区别是:

  • Java 虚拟机的浮点运算不会抛出异常、陷阱或以其他方式发出 IEEE 754 无效运算、被零除、上溢、下溢或不精确等异常情况的信号。 Java 虚拟机没有信号 NaN 值。

  • Java 虚拟机不支持 IEEE 754 信号浮点比较。

  • Java 虚拟机的舍入操作始终使用 IEEE 754 舍入到最接近的模式。不精确的结果四舍五入到最接近的可表示值,并与最低有效位为零的值保持一致。这是 IEEE 754 默认模式。但是将浮点类型的值转换为整型值的 Java 虚拟机指令向零舍入。 Java 虚拟机没有提供任何改变浮点舍入模式的方法。

  • Java 虚拟机不支持 IEEE 754 单扩展或双扩展格式,除非双精度和双扩展指数值集可以说支持单扩展格式。可以选择支持的浮点扩展指数和双扩展指数值集不对应于 IEEE 754 扩展格式的值:IEEE 754 扩展格式需要扩展精度以及扩展指数范围。

Floating-Point Modes 浮点模式

每个方法都有一个浮点模式,要么是 FP-strict,要么不是 FP-strict。方法的浮点模式由定义方法的 method_info 结构(§4.6)的 access_flags 项的 ACC_STRICT 标志的设置决定。设置此标志的方法是 FP-strict;否则,该方法不是 FP 严格的。

请注意,ACC_STRICT 标志的这种映射意味着由 JDK 版本 1.1 或更早版本中的编译器编译的类中的方法实际上不是 FP 严格的。

当其调用创建包含操作数栈的帧的方法具有该浮点模式时,我们将把操作数栈称为具有给定的浮点模式。类似地,当包含该指令的方法具有该浮点模式时,我们会将 Java 虚拟机指令称为具有给定的浮点模式。

如果支持 float-extended-exponent 值集(§2.3.2),则非 FP-strict 操作数堆栈上的 float 类型值的范围可能超过该值集,除非值集转换禁止(§2.8.3) ).如果支持双扩展指数值集(第 2.3.2 节),则非 FP 严格的操作数堆栈上的 double 类型值的范围可能超过该值集,除非被值集转换禁止。

在所有其他上下文中,无论是在操作数堆栈上还是在其他地方,并且无论浮点模式如何,float 和 double 类型的浮点值只能分别在 float 值集和 double 值集范围内。特别是,类和实例字段、数组元素、局部变量和方法参数只能包含从标准值集中提取的值。

Value Set Conversion 值集转换

在特定情况下,允许或要求支持扩展浮点值集的 Java 虚拟机实现在扩展值集和标准值集之间映射关联浮点类型的值。这样的值集转换不是类型转换,而是属于同一类型的值集之间的映射。

在指示值集转换的地方,允许实现对值执行以下操作之一:

如果该值是 float 类型并且不是 float 值集的元素,它将将该值映射到 float 值集的最近元素。

如果该值是双精度类型并且不是双精度值集的元素,则它将值映射到双精度值集的最近元素。

此外,在指示值集转换的地方,需要进行某些操作:

假设执行非 FP-strict 的 Java 虚拟机指令导致 float 类型的值被推入 FP-strict 的操作数堆栈,作为参数传递,或存储到局部变量、字段或数组的元素。如果该值不是浮点值集的元素,它将将该值映射到浮点值集的最近元素。

假设执行非 FP-strict 的 Java 虚拟机指令导致 double 类型的值被压入 FP-strict 的操作数栈,作为参数传递,或存储到局部变量、字段或数组的元素。如果该值不是双精度值集的元素,它将将该值映射到双精度值集中最近的元素。

这种必需的值集转换可能是由于在方法调用期间传递浮点类型的参数而发生的,包括本机方法调用;从非 FP 严格的方法返回浮点类型的值到 FP 严格的方法;或者在非 FP 严格的方法中将浮点类型的值存储到局部变量、字段或数组中。

并非扩展指数值集中的所有值都可以精确映射到相应标准值集中的值。如果映射的值太大而无法准确表示(其指数大于标准值集所允许的值),则将其转换为相应类型的(正或负)无穷大。如果映射的值太小而无法精确表示(其指数小于标准值集所允许的值),则将其四舍五入到最接近的可表示非规范化值或相同符号的零。

值集转换保留无穷大和 NaN,并且不能更改正在转换的值的符号。值集转换对非浮点类型的值没有影响。


Special Methods 特殊方法

在 Java 虚拟机级别,每个用 Java 编程语言(JLS §8.8)编写的构造函数都显示为具有特殊名称 的实例初始化方法。该名称由编译器提供。因为名称 不是有效的标识符,所以它不能直接用在用 Java 编程语言编写的程序中。实例初始化方法只能在 Java 虚拟机中通过 invokespecial 指令(§invokespecial)调用,并且它们只能在未初始化的类实例上调用。实例初始化方法具有派生它的构造函数的访问权限(JLS §6.6)。

一个类或接口最多有一个类或接口初始化方法,并通过调用该方法进行初始化(§5.5)。类或接口的初始化方法具有特殊名称 ,不带任何参数,并且是无效的(§4.3.3)。

类文件中名为 的其他方法无关紧要。它们不是类或接口初始化方法。它们不能被任何 Java 虚拟机指令调用,也永远不会被 Java 虚拟机本身调用。

在版本号为 51.0 或更高版本的类文件中,该方法必须另外设置其 ACC_STATIC 标志(§4.6)才能成为类或接口初始化方法。

此要求是在 Java SE 7 中引入的。在版本号为 50.0 或以下的类文件中,名为 的无效且不带参数的方法被视为类或接口初始化方法,无论其 ACC_STATIC 标志的设置如何.

名称 由编译器提供。因为名称 不是有效的标识符,所以它不能直接用在用 Java 编程语言编写的程序中。 Java 虚拟机隐式调用类和接口初始化方法;它们从不直接从任何 Java 虚拟机指令中调用,而只是作为类初始化过程的一部分间接调用。

如果满足以下所有条件,则方法是签名多态的:

  • 它在 java.lang.invoke.MethodHandle 类中声明。

  • 它有一个 Object[] 类型的形式参数。

  • 它的返回类型为 Object。

  • 它设置了 ACC_VARARGS 和 ACC_NATIVE 标志。

在 Java SE 8 中,唯一的签名多态方法是类 java.lang.invoke.MethodHandle 的 invoke 和 invokeExact 方法。

Java 虚拟机在 invokevirtual 指令 (§invokevirtual) 中对签名多态方法给予特殊处理,以影响方法句柄的调用。方法句柄是对底层方法、构造函数、字段或类似低级操作(第 5.4.3.5 节)的强类型、直接可执行引用,具有参数或返回值的可选转换。这些转换非常通用,包括转换、插入、删除和替换等模式。有关详细信息,请参阅 Java SE 平台 API 中的 java.lang.invoke 包。


Exceptions 异常

Java 虚拟机中的异常由类 Throwable 或其子类之一的实例表示。抛出异常会导致从抛出异常的点立即进行非本地控制转移。

大多数异常是同步发生的,是它们发生的线程的操作的结果。相比之下,异步异常可能发生在程序执行的任何时刻。 Java 虚拟机出于以下三个原因之一抛出异常:

  • 执行了抛出指令 (§athrow)。

  • Java 虚拟机同步检测到异常执行情况。这些异常不会在程序中的任意点抛出,而只会在执行以下指令后同步抛出:

    • 将异常指定为可能的结果,例如:

      • 当指令包含违反 Java 编程语言语义的操作时,例如在数组边界之外进行索引。

      • 当加载或链接部分程序时发生错误。

    • 导致超出资源的某些限制,例如当使用过多的内存时。

  • 发生异步异常是因为:

    • 调用了类 Thread 或 ThreadGroup 的停止方法,或者

    • Java 虚拟机实现中发生内部错误。

一个线程可以调用停止方法来影响另一个线程或指定线程组中的所有线程。它们是异步的,因为它们可能发生在其他线程或多个线程执行的任何时刻。内部错误被认为是异步的(§6.3)。

Java 虚拟机可能允许在抛出异步异常之前进行少量但有限制的执行。允许这种延迟以允许优化的代码在遵守 Java 编程语言的语义的同时在处理它们的实际点处检测并抛出这些异常。

一个简单的实现可能会在每个控制传输指令处轮询异步异常。由于程序的大小是有限的,这就限制了检测异步异常的总延迟。由于在控制传输之间不会发生异步异常,因此代码生成器具有一定的灵活性,可以在控制传输之间重新排序计算以获得更高的性能。论文 Polling Efficiently on Stock Hardware 作者:Marc Feeley,Proc。 1993 年函数式编程和计算机体系结构会议,丹麦哥本哈根,第 179-187 页,推荐作为进一步阅读。

Java 虚拟机抛出的异常是精确的:当控制转移发生时,在抛出异常之前执行的指令的所有效果必须看起来已经发生。在抛出异常的点之后出现的任何指令可能看起来都已被评估。如果优化代码已经推测性地执行了异常发生点之后的一些指令,则此类代码必须准备好将这种推测性执行从程序的用户可见状态中隐藏起来。

Java 虚拟机中的每个方法都可能与零个或多个异常处理程序相关联。异常处理程序指定实现异常处理程序处于活动状态的方法的 Java 虚拟机代码的偏移范围,描述异常处理程序能够处理的异常类型,并指定要处理的代码的位置那个例外。如果导致异常的指令的偏移量在异常处理程序的偏移量范围内,并且异常类型与异常处理程序处理的异常类是同一类或者是异常类的子类,则异常与异常处理程序相匹配。抛出异常时,Java 虚拟机会在当前方法中搜索匹配的异常处理程序。如果找到匹配的异常处理程序,系统将分支到匹配的处理程序指定的异常处理代码。

如果在当前方法中没有找到这样的异常处理程序,则当前方法调用会突然完成(第 2.6.5 节)。在突然完成时,当前方法调用的操作数栈和局部变量被丢弃,它的帧被弹出,恢复调用方法的帧。然后在调用者框架的上下文中重新抛出异常,依此类推,继续方法调用链。如果在到达方法调用链的顶部之前没有找到合适的异常处理程序,则终止抛出异常的线程的执行。

在方法的异常处理程序中搜索匹配项的顺序很重要。在类文件中,每个方法的异常处理程序都存储在一个表中(第 4.7.3 节)。在运行时,当抛出异常时,Java 虚拟机按照它们出现的顺序搜索当前方法的异常处理程序


Instruction Set Summary

Java 虚拟机指令由一个单字节操作码组成,该操作码指定要执行的操作,后跟零个或多个操作数,提供操作使用的参数或数据。许多指令没有操作数,只包含一个操作码。

操作数的数量和大小由操作码决定。如果一个操作数的大小超过一个字节,那么它以大端顺序存储——高位字节在前。例如,局部变量的无符号 16 位索引存储为两个无符号字节,byte1 和 byte2,因此其值为 (byte1 << 8) |字节2。

字节码指令流只是单字节对齐的。两个例外是 lookupswitch 和 tableswitch 指令(§lookupswitch,§tableswitch),它们被填充以强制其某些操作数在 4 字节边界上进行内部对齐。

将 Java 虚拟机操作码限制为一个字节并放弃编译代码中的数据对齐的决定反映了一种有意识的偏向于紧凑性的倾向,这可能是以朴素实现中的一些性能为代价的。一个字节的操作码也限制了指令集的大小。不假设数据对齐意味着大于字节的立即数据必须在许多机器上在运行时从字节构造。

Types and the Java Virtual Machine

Java 虚拟机指令集中的大多数指令都对有关它们执行的操作的类型信息进行编码。例如,iload 指令 (§iload) 将必须为 int 的局部变量的内容加载到操作数堆栈中。 fload 指令 (§fload) 对浮点值执行相同的操作。这两条指令可能具有相同的实现,但具有不同的操作码。

对于大多数类型化指令,指令类型在操作码助记符中用字母明确表示:i 表示 int 操作,l 表示 long,s 表示短,b 表示 byte,c 表示 char,f 表示 float,d 表示 double , 供参考。一些类型明确的指令在它们的助记符中没有类型字母。例如,arraylength 始终对数组对象进行操作。一些指令,例如 goto,无条件控制转移,不对类型化操作数进行操作。

鉴于 Java 虚拟机的一字节操作码大小,将类型编码为操作码对其指令集的设计施加了压力。如果每条类型化指令都支持 Java 虚拟机的所有运行时数据类型,那么指令的数量将超过一个字节所能表示的数量。相反,Java 虚拟机的指令集为某些操作提供了较低级别的类型支持。换句话说,指令集是故意不正交的。必要时,可以使用单独的指令在不受支持和受支持的数据类型之间进行转换。

请注意,表 2.11.1-A 中的大多数指令没有整数类型 byte、char 和 short 的形式。没有一个具有布尔类型的形式。编译器使用 Java 虚拟机指令对 byte 和 short 类型的文字值负载进行编码,这些指令在编译时或运行时将这些值符号扩展为 int 类型的值。使用指令对 boolean 和 char 类型的文字值负载进行编码,这些指令在编译时或运行时将文字零扩展为 int 类型的值。同样,使用 Java 虚拟机指令对 boolean、byte、short 和 char 类型值数组的加载进行编码,这些指令将值符号扩展或零扩展为 int 类型的值。因此,对实际类型 boolean、byte、char 和 short 的值的大多数操作都由对计算类型 int 的值进行操作的指令正确执行。

image.png

Table 2.11.1-B. Actual and Computational types in the Java Virtual Machine

image.png

Load and Store Instructions 加载和存储指令

加载和存储指令在局部变量(§2.6.1)和 Java 虚拟机栈帧(§2.6)的操作数堆栈(§2.6.2)之间传输值:

将局部变量加载到操作数堆栈:iload、iload_、lload、lload_、fload、fload_、dload、dload_、aload、aload_。

将操作数堆栈中的值存储到局部变量中:istore、istore_、lstore、lstore_、fstore、fstore_、dstore、dstore_、astore、astore_。

将常量加载到操作数堆栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_、lconst_、fconst_、dconst_。

使用更宽的索引或更大的直接操作数访问更多局部变量:wide。

访问对象字段和数组元素的指令(第 2.11.5 节)也将数据传入和传出操作数栈。

上面显示的带有尖括号之间尾随字母的指令助记符(例如,iload_)表示指令族(在 iload_ 的情况下,具有成员 iload_0、iload_1、iload_2 和 iload_3)。此类指令族是采用一个操作数的附加通用指令 (iload) 的特化。对于专用指令,操作数是隐式的,不需要存储或取出。语义在其他方面是相同的(iload_0 与操作数为 0 的 iload 意思相同)。尖括号之间的字母指定该系列指令的隐式操作数的类型:对于 ,一个非负整数;对于,一个整数;对于 ,很长;对于 ,一个浮点数;对于 ,一个双精度值。 int 类型的形式在许多情况下用于对 byte、char 和 short 类型的值执行操作(第 2.11.1 节)。

整个规范中都使用了这种指令族符号。

Arithmetic Instructions 算术指令

算术指令计算的结果通常是操作数堆栈上两个值的函数,将结果推回操作数堆栈。有两种主要的算术指令:对整数值进行运算的指令和对浮点值进行运算的指令。在每一种类型中,算术指令专门用于 Java 虚拟机数字类型。不直接支持对 byte、short 和 char 类型的值(第 2.11.1 节)或布尔类型的值进行整数运算;这些操作由对 int 类型进行操作的指令处理。整数和浮点指令在溢出和被零除时的行为也不同。算术指令如下:

添加: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、luushr。

按位或:ior, lor。

按位与:iand,土地。

按位异或:ixor,lxor。

局部变量增量:iinc。

比较:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。

Java 编程语言运算符对整数和浮点值的语义(JLS §4.2.2,JLS §4.2.4)直接由 Java 虚拟机指令集的语义支持。

Java 虚拟机在对整数数据类型进行操作时不会指示溢出。唯一可以抛出异常的整数运算是整数除法指令(idiv 和 ldiv)和整数余数指令(irem 和 lrem),如果除数为零,它们将抛出 ArithmeticException。

Java 虚拟机对浮点数的操作符合 IEEE 754 中的规定。特别是,Java 虚拟机需要完全支持 IEEE 754 非规范化浮点数和渐进下溢,这使得证明特定数值算法的理想属性变得更加容易.

Java 虚拟机要求浮点运算的行为就像每个浮点运算符都将其浮点结果四舍五入到结果精度一样。不精确的结果必须舍入到最接近无限精确结果的可表示值;如果两个最接近的可表示值同样接近,则选择最低有效位为零的值。这是 IEEE 754 标准的默认舍入模式,称为舍入到最近模式。

Java 虚拟机在将浮点值转换为整数时使用 IEEE 754 向零舍入模式。这导致数字被截断;表示操作数值的小数部分的有效数字的任何位都将被丢弃。向零舍入模式选择最接近但幅度不大于无限精确结果的类型值作为其结果。

Java 虚拟机的浮点运算符不会抛出运行时异常(不要与 IEEE 754 浮点异常混淆)。上溢运算产生有符号无穷大,下溢运算产生非规范化值或有符号零,没有数学确定结果的运算产生 NaN。所有以 NaN 作为操作数的数值运算都会产生 NaN 作为结果。

对 long (lcmp) 类型值的比较执行带符号的比较。使用 IEEE 754 非信号比较执行浮点类型(dcmpg、dcmpl、fcmpg、fcmpl)值的比较。

Type Conversion Instructions 类型转换指令

类型转换指令允许在 Java 虚拟机数字类型之间进行转换。这些可用于在用户代码中实现显式转换或缓解 Java 虚拟机指令集中缺乏正交性的问题。

Java 虚拟机直接支持以下扩展数字转换:

int 到 long、float 或 double

长期浮动或加倍

浮动加倍

扩大的数值转换指令是 i2l、i2f、i2d、l2f、l2d 和 f2d。考虑到类型指令的命名约定和双关语使用 2 表示“to”,这些操作码的助记符很简单。例如,i2d 指令将 int 值转换为 double。

大多数扩大数值转换不会丢失有关数值总体大小的信息。实际上,从 int 到 long 以及从 int 到 double 的转换根本不会丢失任何信息;数值被准确保留。从 float 扩大到 double 的转换是 FP-strict(§2.8.2)也准确地保留了数值;只有非 FP 严格的此类转换可能会丢失有关转换值的总体大小的信息。

从 int 到 float,或从 long 到 float,或从 long 到 double 的转换可能会丢失精度,也就是说,可能会丢失值的一些最低有效位;生成的浮点值是整数值的正确舍入版本,使用 IEEE 754 舍入到最接近的模式。

尽管可能会丢失精度,但扩大数字转换永远不会导致 Java 虚拟机抛出运行时异常(不要与 IEEE 754 浮点异常混淆)。

从 int 到 long 的扩展数字转换只是符号扩展 int 值的二进制补码表示以填充更宽的格式。一个 char 到整数类型的扩展数字转换零扩展 char 值的表示以填充更宽的格式。

请注意,不存在从整数类型 byte、char 和 short 到 int 类型的扩展数字转换。如 §2.11.1 中所述,byte、char 和 short 类型的值在内部扩展为 int 类型,从而使这些转换成为隐式的。

Java 虚拟机还直接支持以下窄化数字转换:

int 到 byte、short 或 char

长整型

浮动到 int 或 long

double 到 int、long 或 float

缩小数值转换指令是 i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。缩小数字转换可能会导致值的符号不同、数量级不同或两者兼而有之;它可能因此失去精度。

从 int 或 long 到整数类型 T 的缩小数字转换简单地丢弃除 n 个最低位以外的所有位,其中 n 是用于表示类型 T 的位数。这可能导致结果值不具有相同的符号作为输入值。

在将浮点值缩小为整数类型 T 的数值转换中,其中 T 为 int 或 long,浮点值转换如下:

如果浮点值是 NaN,转换的结果是 int 或 long 0。

否则,如果浮点值不是无穷大,则使用 IEEE 754 向零舍入模式将浮点值舍入为整数值 V。有两种情况:

如果 T 是 long 并且这个整数值可以表示为 long,那么结果就是 long 值 V。

如果 T 是 int 类型,并且这个整数值可以表示为 int,那么结果就是 int 值 V。

否则:

该值必须太小(幅度很大的负值或负无穷大),并且结果是 int 或 long 类型的最小可表示值。

或者该值必须太大(幅度很大的正值或正无穷大),结果是 int 或 long 类型的最大可表示值。

从 double 到 float 的缩小数字转换行为符合 IEEE 754。使用 IEEE 754 舍入到最近模式正确舍入结果。太小而无法表示为浮点数的值将转换为浮点类型的正或负零;太大而无法表示为浮点数的值将转换为正无穷大或负无穷大。 double NaN 总是转换为 float NaN。

尽管可能会发生上溢、下溢或精度损失,但缩小数字类型之间的转换永远不会导致 Java 虚拟机抛出运行时异常(不要与 IEEE 754 浮点异常混淆)。

Object Creation and Manipulation 对象创建和操作

尽管类实例和数组都是对象,但 Java 虚拟机使用不同的指令集创建和操作类实例和数组:

  • 创建一个新的类实例:new。

  • 创建一个新数组:newarray、anewarray、multiawarray。

  • 访问类的字段(静态字段,称为类变量)和类实例的字段(非静态字段,称为实例变量):getstatic、putstatic、getfield、putfield。

  • 将数组组件加载到操作数堆栈:baload、caload、saload、iaload、laload、faload、daload、aaload。

  • 将操作数堆栈中的值存储为数组组件:bastore、castore、sastore、iastore、lastore、fastore、dastore、aastore。

  • 获取数组的长度:arraylength。

  • 检查类实例或数组的属性:instanceof、checkcast。

Operand Stack Management Instructions 操作数栈管理指令

为直接操作操作数堆栈提供了许多指令:pop、pop2、dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2、swap。

Control Transfer Instructions 控制转移指令

控制转移指令有条件或无条件地使 Java 虚拟机继续执行控制转移指令之后的指令以外的指令。他们是:

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

复合条件分支:tableswitch、lookupswitch。

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

Java 虚拟机具有不同的指令集,这些指令集在与 int 和引用类型的数据进行比较时有条件地分支。它还具有用于测试 null 引用的不同条件分支指令,因此不需要为 null 指定具体值(§2.4)。

使用 int 比较指令(第 2.11.1 节)执行 boolean、byte、char 和 short 类型数据之间比较的条件分支。比较数据类型 long、float 或 double 的条件分支是使用比较数据并生成 int 比较结果的指令启动的(第 2.11.3 节)。随后的 int 比较指令测试此结果并影响条件分支。由于强调 int 比较,Java 虚拟机为类型 int 提供了丰富的条件分支指令补充。

所有 int 条件控制传输指令都执行带符号的比较。

Method Invocation and Return Instructions 方法调用和返回指令

以下五个指令调用方法:

  • invokevirtual 调用对象的实例方法,调度对象的(虚拟)类型。这是 Java 编程语言中的正常方法分派。

  • invokeinterface 调用接口方法,搜索由特定运行时对象实现的方法以找到合适的方法。

  • invokespecial 调用需要特殊处理的实例方法,无论是实例初始化方法(§2.9)、私有方法还是超类方法。

  • invokestatic 调用命名类中的类(静态)方法。

  • invokedynamic 调用作为绑定到 invokedynamic 指令的调用站点对象的目标的方法。作为在指令第一次执行之前运行引导方法的结果,调用站点对象被 Java 虚拟机绑定到 invokedynamic 指令的特定词法出现。因此,与调用方法的其他指令不同,invokedynamic 指令的每次出现都具有唯一的链接状态。

  • 方法返回指令,按返回类型区分,有ireturn(用于返回boolean、byte、char、short、int类型的值)、lreturn、freturn、dreturn、areturn。此外,return 指令用于从声明为 void 的方法、实例初始化方法以及类或接口初始化方法中返回。

Throwing Exceptions 抛出异常

使用 athrow 指令以编程方式抛出异常。如果检测到异常情况,各种 Java 虚拟机指令也可以抛出异常。

Synchronization

Java 虚拟机通过一个同步结构支持方法和方法内指令序列的同步:监视器。

方法级同步是隐式执行的,作为方法调用和返回的一部分(第 2.11.8 节)。同步方法在运行时常量池的 method_info 结构(第 4.6 节)中通过 ACC_SYNCHRONIZED 标志进行区分,该标志由方法调用指令检查。当调用设置了 ACC_SYNCHRONIZED 的方法时,执行线程进入监视器,调用方法本身,然后退出监视器,无论方法调用是正常完成还是突然完成。在执行线程拥有监视器期间,没有其他线程可以进入它。如果在调用synchronized方法时抛出异常,并且synchronized方法没有处理该异常,则在synchronized方法重新抛出异常之前自动退出该方法的监视器。

指令序列的同步通常用于对 Java 编程语言的同步块进行编码。 Java 虚拟机提供了 monitorenter 和 monitorexit 指令来支持这种语言结构。同步块的正确实现需要来自以 Java 虚拟机为目标的编译器的合作(§3.14)。

结构化锁定是指在方法调用期间,给定监视器上的每个出口都与该监视器上的前一个条目相匹配的情况。由于无法保证提交给 Java 虚拟机的所有代码都将执行结构化锁定,因此允许但不要求 Java 虚拟机的实现强制执行以下两条保证结构化锁定的规则。假设 T 是一个线程,M 是一个监视器。然后:

在方法调用期间,T 在 M 上执行的监视器条目数必须等于在方法调用期间 T 在 M 上执行的监视器退出数,无论方法调用是正常完成还是突然完成。

在方法调用期间,自方法调用以来 T 对 M 执行的监视器退出的次数绝不能超过自方法调用以来 T 对 M 执行的监视器条目的次数。

请注意,在调用同步方法时由 Java 虚拟机自动执行的监视器进入和退出被认为是在调用方法的调用期间发生的。


Class Libraries 类库

Java 虚拟机必须为 Java SE 平台类库的实现提供足够的支持。这些库中的一些类离不开Java虚拟机的配合是无法实现的。

可能需要 Java 虚拟机特殊支持的类包括支持:

反射,例如包java.lang.reflect中的类和类Class。

加载和创建类或接口。最明显的例子是类 ClassLoader。

类或接口的链接和初始化。上面引用的示例类也属于这一类。

安全性,例如包java.security中的类和其他类,例如SecurityManager。

多线程,例如 Thread 类。

弱引用,例如包 java.lang.ref 中的类。

上面的列表是说明性的,而不是全面的。这些类或它们提供的功能的详尽列表超出了本规范的范围。有关详细信息,请参阅 Java SE 平台类库的规范。


Public Design, Private Implementation

到目前为止,该规范已经勾勒出 Java 虚拟机的公共视图:类文件格式和指令集。这些组件对于 Java 虚拟机的硬件、操作系统和实现独立性至关重要。实施者可能更愿意将它们视为一种在每个实现 Java SE 平台的主机之间安全地传递程序片段的方法,而不是将其视为需要严格遵循的蓝图。

了解公共设计和私有实施之间的界限在哪里很重要。 Java 虚拟机实现必须能够读取类文件,并且必须准确地实现其中的 Java 虚拟机代码的语义。这样做的一种方法是将此文档作为规范并逐字执行该规范。但是,实现者在本规范的约束范围内修改或优化实现也是完全可行和可取的。只要可以读取类文件格式并保持其代码的语义,实现者就可以以任何方式实现这些语义。 “幕后”是实施者的事,只要仔细维护正确的外部接口即可。

有一些例外:调试器、分析器和即时代码生成器都可能需要访问通常被认为是“底层”的 Java 虚拟机元素。在适当的情况下,Oracle 与其他 Java 虚拟机实施者和工具供应商合作,开发 Java 虚拟机的通用接口以供此类工具使用,并在整个行业推广这些接口。

实现者可以使用这种灵活性来定制 Java 虚拟机实现以实现高性能、低内存使用或可移植性。在给定的实现中什么有意义取决于该实现的目标。实施选项的范围包括以下内容:

在加载时或执行期间将 Java 虚拟机代码翻译成另一个虚拟机的指令集。

在加载时或执行期间将 Java 虚拟机代码转换为主机 CPU 的本机指令集(有时称为即时或 JIT 代码生成)。

精确定义的虚拟机和目标文件格式的存在不需要显着限制实施者的创造力。 Java 虚拟机旨在支持许多不同的实现,提供新的和有趣的解决方案,同时保持实现之间的兼容性。


人在困难面前本能的第一个反应就是放弃,可能是出于对失败的恐惧或者对将要面对的困难的恐惧。

lADPKHtEUSkNUBjNBF3NA9I_978_1117.jpg

陈皓(左耳朵耗子)资深 IT 技术专家于 2023年5月13日因突发心梗于上周六晚间离世,年仅 47 岁。谨以此悼念陈皓,一路走好。R.I.P.

image.png


DevX 会持续有趣的技术和见闻,如果你觉得本文对你有帮助希望你可以分享给更多的朋友看到。该文章会同步在微信公众号 【DevXJava】, 方便在微信客户端阅读。

DevX 不止于技术