原文
https://docs.oracle.com/javase/specs/jvms/se21/html/index.html
1简介
2 Java虚拟机的结构
本文档指定了一个抽象机。它没有描述 Java 虚拟机的任何特定实现。
要正确实现Java虚拟机,您只需要能够读取class文件格式并正确执行其中指定的操作即可。不属于 Java 虚拟机规范的实现细节将不必要地限制实现者的创造力。例如,运行时数据区域的内存布局、使用的垃圾收集算法以及 Java 虚拟机指令的任何内部优化(例如,将它们转换为机器代码)都由实现者自行决定。
本规范中对 Unicode 的所有引用均根据Unicode 标准版本 15.0给出,可从 获取https://www.unicode.org/。
2.1 文件class格式
由 Java 虚拟机执行的编译代码使用独立于硬件和操作系统的二进制格式表示, 通常(但不一定)存储在文件中,称为文件格式class。 文件class格式精确定义了类或接口的表示,包括字节顺序等细节,这些细节在特定于平台的目标文件格式中可能被认为是理所当然的。
第 4 章“class文件格式”class详细介绍了文件格式。
2.2. 数据类型
与 Java 编程语言一样,Java 虚拟机对两种类型进行操作:基元类型和引用类型。 相应地,可以存储在变量中、作为参数传递、由方法返回和操作的值也有两种:基元值和引用值。
Java 虚拟机希望几乎所有的类型检查都在运行前完成,通常由编译器完成,Java 虚拟机本身无需进行类型检查。 原始类型的值无需标记,也无需以其他方式进行检查,以便在运行时确定其类型,或将其与引用类型的值区分开来。相反,Java 虚拟机的指令集会使用旨在对特定类型的值进行操作的指令来区分其操作数类型。例如,iadd、ladd、fadd 和 dadd 都是 Java 虚拟机指令,用于将两个数值相加并产生数值结果,但每条指令都针对其操作数类型(分别为 int、long、float 和 double)进行了专门处理。有关 Java 虚拟机指令集的类型支持概要,请参阅第 2.11.1 节。
Java 虚拟机包含对对象的明确支持。对象是动态分配的类实例或数组。对对象的引用被视为 Java 虚拟机类型引用。引用类型的值可视为指向对象的指针。 一个对象可能存在多个引用。对象总是通过类型引用的值进行操作、传递和测试。
2.3.原始类型和值
Java 虚拟机支持的基本数据类型是 数字类型、boolean类型(第 2.3.4 节)和returnAddress类型(第 2.3.3 节)。
数字类型由整型 (§2.3.1)和浮点类型(§2.3.2)组成。
积分类型有:
byte,其值为 8 位有符号补码整数,默认值为 0
short,其值为 16 位有符号补码整数,默认值为 0
int,其值为 32 位有符号二进制补码整数,其默认值为零
long,其值为 64 位有符号二进制补码整数,其默认值为零
char,其值为 16 位无符号整数,表示基本多语言平面中的 Unicode 代码点,使用 UTF-16 编码,其默认值为空代码点 ( '\u0000')
浮点类型有:
float,其值与 32 位 IEEE 754 二进制 32 格式表示的值完全对应,并且其默认值为正零
double,其值与 64 位 IEEE 754 二进制 64 格式的值完全对应,其默认值为正零
该boolean类型的值对真值true和 进行编码false,默认值为false。
第一版Java ® 虚拟机规范并不认为 boolean是 Java 虚拟机类型。然而,boolean值在 Java 虚拟机中的支持确实有限。《Java ®虚拟机规范》第二版通过将其视为boolean一种类型 来澄清该问题。
该returnAddress类型的值是指向 Java 虚拟机指令的操作码的指针。在原始类型中,只有returnAddress类型不与 Java 编程语言类型直接关联。
2.3.1. 整数类型和值
Java虚拟机的整数类型的值为:
对于byte,从 -128 到 127(-2 7到 2 7 - 1),包括边界值
对于short,从 -32768 到 32767(-2 15到 2 15 - 1),包括边界值
对于int,从 -2147483648 到 2147483647(-2 31到 2 31 - 1),包括边界值
对于long,从 -9223372036854775808 到 9223372036854775807(-2 63到 2 63 - 1),包括边界值
对于char,从 0 到 65535(含)
2.3.2. 浮点类型和值
浮点类型为float和double, 它们在概念上与 IEEE 754 值和运算的 32 位 binary32 和 64 位 binary64 浮点格式相关联,如 IEEE 754 标准 (JLS §1.7) 中所指定。
在 Java SE 15 及更高版本中,Java 虚拟机使用 2019 版 IEEE 754 标准。 在 Java SE 15 之前,Java 虚拟机使用 1985 版本的 IEEE 754 标准,其中 binary32 格式称为单格式,binary64 格式称为双格式。
IEEE 754 不仅包括由符号和大小组成的正数和负数,还包括正零和负零、 正无穷大和负无穷大以及特殊的非数字值(以下缩写为 NaN)。NaN 值用于表示某些无效运算的结果,例如零除零。 float和类型的 NaN 常量double预定义为 Float.NaN和Double.NaN。
浮点类型的有限非零值都可以用 s ⋅ m ⋅ 2 ( e - N+ 1)的形式表示,其中:
s为+1 或-1,
m是小于 2 的正整数N,
e是E min = -(2 K -1 -2) 和 E max = 2 K -1 -1之间的整数( 含),并且
N和K是取决于类型的参数。
某些值可以通过多种方式以这种形式表示。 例如,假设v浮点类型的值可以使用s、m和 e的某些值以这种形式表示,那么如果碰巧m是偶数且e小于 2 K -1,则可以将m减半并将e加 1,以生成相同值的第二个表示形式v。
如果m ≥ 2 -1 ,则 这种形式的表示称为 归一化;否则该表示被认为是次正规的。 如果浮点类型的值不能以m ≥ 2 -1的方式表示,则该值被称为次正规值,因为其大小低于最小标准化值的大小。 N N
表 2.3.2-A总结了和的参数N和K(以及导出的参数E min和E max) 的约束。 floatdouble
表 2.3.2-A。浮点参数
范围 float double N 24 53 K 8 11 最大E +127 +1023 最小E -126 -1022 除 NaN 外,浮点值都是有序的。从最小到最大排列时,它们是负无穷大、负有限非零值、正零和负零、正有限非零值和正无穷大。
IEEE 754 允许其每个二进制 32 和二进制 64 浮点格式有多个不同的 NaN 值。 然而,Java SE 平台通常将给定浮点类型的 NaN 值视为折叠为单个规范值,因此本规范通常将任意 NaN 视为规范值。
在 IEEE 754 下,具有非 NaN 参数的浮点运算可能会生成 NaN 结果。 IEEE 754 指定了一组 NaN 位模式,但不强制使用哪种特定 NaN 位模式来表示 NaN 结果;这是留给硬件架构的。 程序员可以创建具有不同位模式的 NaN 来编码,例如,回顾性诊断信息。 这些 NaN 值可以分别使用和 的 Float.intBitsToFloat和 Double.longBitsToDouble方法创建 。 相反,要检查 NaN 值的位模式,和 方法可分别用于 和。 floatdoubleFloat.floatToRawIntBitsDouble.doubleToRawLongBitsfloatdouble
正零和负零比较相等,但还有其他操作可以区分它们;例如,除以 1.0产生0.0正无穷大,但除以1.0产生 -0.0负无穷大。
NaN 是无序的false,因此数值比较和数值相等测试如果其中一个或两个操作数都是 NaN,则具有值。 false特别是,当且仅当该值为 NaN 时,对值与其自身进行数值相等的测试才具有该值。 true如果任一操作数为 NaN,则 数值不等式测试具有该值。
2.3.3. 类型returnAddress和值
该returnAddress类型由 Java 虚拟机的jsr、ret和jsr_w指令(§ jsr、§ ret、 § jsr_w)使用。 该returnAddress 类型的值是指向 Java 虚拟机指令的操作码的指针。 与数字基元类型不同,该returnAddress类型不对应于任何 Java 编程语言类型,并且不能由正在运行的程序修改。
2.3.4. 类型boolean_
尽管Java虚拟机定义了 boolean类型,但它只提供非常有限的支持。 不存在专门用于boolean 值操作的 Java 虚拟机指令。 相反,Java 编程语言中对 boolean值进行操作的表达式被编译为使用 Java 虚拟机int数据类型的值。
Java 虚拟机直接支持boolean数组。它的newarray指令(§ newarray)可以创建boolean 数组。 使用数组指令baload和bastore (§ baload、 § bastore) boolean访问和修改 类型的数组。byte
在 Oracle 的 Java 虚拟机实现中,booleanJava 编程语言中的数组被编码为 Java 虚拟机byte数组,每个 boolean元素使用 8 位。
Java 虚拟机使用to 表示和to 表示 来对boolean 数组组件进行编码。当编译器将 Java 编程语言值映射到 Java 虚拟机类型的值时,编译器必须使用相同的编码。 1true0falsebooleanint
2.4. 参考类型和值
类型分为三种reference :类类型、数组类型和接口类型。 它们的值分别是对动态创建的类实例、数组或实现接口的类实例或数组的引用。
数组类型由 具有单一维度的组件类型组成(其长度不是由类型给出的)。数组类型的组件类型本身可以是数组类型。 如果从任何数组类型开始,考虑其组件类型,然后(如果这也是数组类型)该类型的组件类型,依此类推,最终必须到达不是数组类型的组件类型; 这称为数组类型的元素类型。数组类型的元素类型必须是基元类型、类类型或接口类型。
值reference也可以是特殊的空引用,即对无对象的引用,此处用 表示null。 该null引用最初没有运行时类型,但可以转换为任何类型。reference类型的默认值为null。
本规范不强制要求具体的值编码null。
2.5. 运行时数据区
Java 虚拟机定义了程序执行期间使用的各种运行时数据区域。 其中一些数据区域是在 Java 虚拟机启动时创建的,并且仅在 Java 虚拟机终止时才被销毁。 其他数据区域是每个线程的。每个线程的数据区域在创建线程时创建,并在线程终止时销毁。
2.5.1. pc register(PC寄存器)
Java 虚拟机可以同时支持多个执行线程(JLS §17)。 每个Java虚拟机线程都有自己的 pc(程序计数器)寄存器。 在任何时候,每个 Java 虚拟机线程都在执行单个方法的代码,即该线程的当前方法(第 2.6 节)。 如果该方法不是 native,则pc寄存器包含当前正在执行的 Java 虚拟机指令的地址。 如果线程当前正在执行的方法是native,则Java虚拟机寄存器的值pc 是未定义的。 Java 虚拟机的pc寄存器足够宽,可以容纳returnAddress特定平台上的一个或一个本机指针。
2.5.2. Java 虚拟机栈
每个Java虚拟机线程都有一个私有的Java虚拟机栈,与线程同时创建。 Java 虚拟机栈存储帧(§2.6)。 Java虚拟机栈类似于C等传统语言的栈:它保存局部变量和部分结果,并在方法调用和返回中发挥作用。 由于除了推送和弹出帧之外,Java 虚拟机栈从不直接操作,因此帧可能是堆分配的。Java 虚拟机栈的内存不需要是连续的。
在Java®虚拟机规范第一版中,Java 虚拟机栈被称为Java栈。
该规范允许 Java 虚拟机栈具有固定大小或根据计算的需要动态扩展和收缩。 如果Java虚拟机栈具有固定大小,则每个Java虚拟机栈的大小可以在创建该栈时独立选择。
Java虚拟机实现可以为程序员或用户提供对Java虚拟机栈的初始大小的控制,以及在动态扩展或收缩Java虚拟机栈的情况下,对最大和最小大小的控制。
以下异常情况与 Java 虚拟机堆栈相关:
如果线程中的计算需要比允许的更大的 Java 虚拟机栈,则 Java 虚拟机会抛出StackOverflowError.
如果 Java 虚拟机栈可以动态扩展,并且尝试扩展但没有足够的内存来实现扩展, 或者如果没有足够的内存来为新线程创建初始 Java 虚拟机栈, 则 Java 虚拟机栈机器抛出一个OutOfMemoryError.
2.5.3. 堆
Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的堆。堆是运行时数据区域,所有类实例和数组的内存都从这里分配。
堆是在虚拟机启动时创建的。对象的堆存储由自动存储管理系统(称为垃圾收集器)回收;对象永远不会被显式释放。 Java虚拟机不假设特定类型的自动存储管理系统,并且可以根据实现者的系统要求来选择存储管理技术。 堆可以是固定大小的,或者可以根据计算的需要进行扩展,并且如果不需要更大的堆,则可以收缩。堆的内存不需要是连续的。
Java虚拟机实现可以为程序员或用户提供对堆的初始大小的控制,并且如果堆可以动态扩展或收缩,则可以对最大和最小堆大小进行控制。
以下异常情况与堆相关:
如果计算需要的堆多于自动存储管理系统所能提供的堆,Java 虚拟机将抛出一个 OutOfMemoryError.
2.5.4. 方法区
Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区。 方法区类似于传统语言的编译代码的存储区或者类似于操作系统进程中的“文本”段。 它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和接口初始化以及实例初始化中使用的特殊方法(第 2.9 节)。
方法区是在虚拟机启动时创建的。尽管方法区在逻辑上是堆的一部分,但简单的实现可以选择不进行垃圾收集或压缩它。 本规范不强制要求方法区的位置或用于管理编译代码的策略。 方法区可以是固定大小的,或者可以根据计算的需要来扩展,并且如果不需要更大的方法区,则可以收缩。方法区的内存不需要是连续的。
Java虚拟机实现可以为程序员或用户提供对方法区的初始大小的控制,以及在方法区大小变化的情况下,对最大和最小方法区大小的控制。
以下异常情况与方法区相关:
如果方法区中的内存无法满足分配请求,Java 虚拟机将抛出一个OutOfMemoryError.
2.5.5。运行时常量池
运行时常量池是constant_pool文件中表的每个类或每个接口的运行时表示class(第 4.4 节)。 它包含多种常量,范围从编译时已知的数字文字到必须在运行时解析的方法和字段引用。 运行时常量池的功能类似于传统编程语言的符号表,尽管它包含比典型符号表更广泛的数据。
每个运行时常量池都是从 Java 虚拟机的方法区(第 2.5.4 节)分配的。类或接口的运行时常量池是在Java 虚拟机 创建类或接口(第 5.3 节)时构造的。
以下异常情况与类或接口的运行时常量池的构造相关:
创建类或接口时,如果运行时常量池的构造需要的内存多于 Java 虚拟机方法区中可用的内存,Java 虚拟机将抛出一个OutOfMemoryError.
有关运行时常量池构造的信息, 请参阅§5(加载、链接和初始化) 。
2.5.6。本机方法栈
Java虚拟机的实现可以使用传统的栈(通俗地称为“C栈”)来支持native方法(以除Java编程语言之外的语言编写的方法)。 本机方法栈也可以由 Java 虚拟机指令集的解释器的实现使用,例如 C 语言。 无法加载方法并且本身不依赖于常规栈的 Java 虚拟机实现不需要提供本机方法native 栈。 如果提供了本机方法栈,则通常会在创建每个线程时为每个线程分配本机方法栈。
该规范允许本机方法栈具有固定大小或根据计算的需要动态扩展和收缩。如果本机方法栈具有固定大小,则每个本机方法栈的大小可以在创建该栈时独立选择。
Java虚拟机实现可以为程序员或用户提供对本机方法栈的初始大小的控制,以及在不同大小的本机方法栈的情况下,对最大和最小方法栈大小的控制。
以下异常情况与本机方法栈相关:
如果线程中的计算需要比允许的更大的本机方法栈,则 Java 虚拟机会抛出StackOverflowError.
如果可以动态扩展本机方法栈并尝试扩展本机方法栈,但可用内存不足,或者没有足够的内存来为新线程创建初始本机方法栈,则 Java 虚拟机将抛出OutOfMemoryError.
2.6。frames
框架 用于存储数据和部分结果,以及执行动态链接、方法的返回值和分派异常 。
每次调用方法时都会创建一个新框架。当其方法调用完成时,框架将被销毁,无论该完成是正常完成还是突然完成(它会引发未捕获的异常)。 帧是从创建帧的线程的Java 虚拟机栈(第 2.5.2 节)分配的。 每个帧都有自己的局部变量数组(第 2.6.1 节)、自己的操作数栈(第 2.6.2 节)以及对当前方法的类的运行时常量池(第 2.5.5 节)的引用。
帧可以用附加的特定于实现的信息来扩展,例如调试信息。
局部变量数组和操作数栈的大小在编译时确定,并与与帧关联的方法的代码一起提供(第 4.7.3 节)。 因此,帧数据结构的大小仅取决于Java虚拟机的实现,并且这些结构的内存可以在方法调用时同时分配。
在给定的控制线程中,只有一帧(执行方法的帧)在任何点处于活动状态。 该帧称为当前帧,其方法称为当前方法。 定义当前方法的类是当前类。对局部变量和操作数栈的操作通常参考当前帧。
如果一个框架的方法调用另一个方法或者它的方法完成,那么该框架就不再是当前的。 调用方法时,将创建一个新框架,并在控制权转移到新方法时成为当前框架。 在方法返回时,当前帧将其方法调用的结果(如果有)传回前一帧。 当前一帧成为当前帧时,当前帧将被丢弃。
请注意,线程创建的帧是该线程本地的,不能被任何其他线程引用。
2.6.1. 局部变量
每个帧(第 2.6节)包含一个称为局部变量的变量数组。 框架的局部变量数组的长度在编译时确定,并以类或接口的二进制表示形式以及与框架关联的方法的代码提供(第 4.7.3 节)。
boolean单个局部变量可以保存、byte、char、short、int、 float、reference、 或类型的值returnAddress。 一对局部变量可以保存类型为long或的值double。
局部变量通过索引来寻址。第一个局部变量的索引为零。 当且仅当该整数介于零和比局部变量数组的大小小一之间时,该整数才被视为局部变量数组的索引。
long一个type或type的值double占用两个连续的局部变量。 这样的值只能使用较小的索引来寻址。 例如,存储在索引为ndouble的局部变量数组中的类型值 实际上占用了索引为n和 n +1的局部变量; 但是,无法加载索引n +1处的局部变量。可以将其存储到. 但是,这样做会使局部变量n的内容无效。
Java虚拟机不要求 n是偶数。 直观地说,类型long和 的值double不需要在局部变量数组中进行 64 位对齐。 实现者可以自由决定使用为该值保留的两个局部变量来表示这些值的适当方式。
Java 虚拟机使用局部变量在方法调用时传递参数。 在类方法调用时,任何参数都以从局部变量0开始的连续局部变量传递。 在实例方法调用时,局部变量0始终用于传递对正在调用实例方法的对象的引用(this在 Java 编程语言中)。 随后,所有参数都会在从局部变量1开始的连续局部变量中传递。
2.6.2. 操作数栈
每个帧(§2.6)包含一个后进先出(LIFO)堆栈,称为操作数堆栈。帧的操作数堆栈的最大深度在编译时确定,并与与帧关联的方法的代码一起提供(第 4.7.3 节)。
在上下文清楚的情况下,我们有时会将当前帧的操作数堆栈简称为操作数堆栈。
创建包含操作数堆栈的帧时,操作数堆栈为空。Java 虚拟机提供将局部变量或字段中的常量或值加载到操作数堆栈上的指令。其他 Java 虚拟机指令从操作数堆栈中获取操作数,对其进行操作,然后将结果推回操作数堆栈。操作数堆栈还用于准备要传递给方法的参数以及接收方法结果。
例如,iadd 指令 ( § iadd ) 将两个int值相加。它要求int要添加的值是操作数堆栈的顶部两个值,由先前的指令推送到那里。这两个int值都从操作数堆栈中弹出。它们被相加,并且它们的和被推回操作数栈。子计算可以嵌套在操作数堆栈上,从而产生可由包含计算使用的值。
操作数堆栈上的每个条目都可以保存任何 Java 虚拟机类型的值,包括 type long或 type的值double。
操作数堆栈中的值必须以适合其类型的方式进行操作。例如,不可能推入两个int值,然后将它们视为 a long,或者推入两个值,然后使用iaddfloat指令将它们相加。少量 Java 虚拟机指令(dup指令 ( § dup ) 和swap ( § swap ))将运行时数据区域作为原始值进行操作,而不考虑其具体类型;这些指令的定义方式使其不能用于修改或分解单个值。这些对操作数堆栈操作的限制是通过文件验证强制执行的(第 4.10 节)。 class
在任何时间点,操作数堆栈都具有关联的深度,其中 或 类型的值long贡献 double两个单位的深度,而任何其他类型的值贡献一个单位的深度。
2.6.3. 动态链接
每个帧(§2.6 )包含对当前方法类型的运行时常量池( §2.5.5 )的引用,以支持方法代码的动态链接。class方法的文件代码是指通过符号引用调用的方法和访问的变量。动态链接将这些符号方法引用转换为具体方法引用,根据需要加载类以解析尚未定义的符号,并将变量访问转换为与这些变量的运行时位置关联的存储结构中的适当偏移量。
方法和变量的这种后期绑定使得方法使用的其他类中的更改不太可能破坏此代码。
2.6.4. 正常方法调用完成
如果方法调用 不会直接从 Java 虚拟机或由于执行显式 语句而引发异常(第 2.10 节),则该方法调用正常完成。如果当前方法的调用正常完成,则可以向调用方法返回一个值。当调用的方法执行返回指令之一(第 2.11.8 节)时,会发生这种情况,该指令的选择必须适合返回值的类型(如果有)。 throw
在这种情况下,当前帧(§2.6)用于恢复调用者的状态,包括其局部变量和操作数堆栈,调用者的程序计数器适当递增以跳过方法调用指令。然后,执行在调用方法的帧中正常继续,并将返回值(如果有)推送到该帧的操作数堆栈上。
2.6.5。方法调用突然完成
如果方法 内执行 Java 虚拟机指令导致 Java 虚拟机抛出异常(第 2.10 节),并且方法内未处理该异常,则方法调用会突然完成。执行athrow指令 ( §athrow ) 也会导致显式抛出异常,并且如果当前方法没有捕获该异常,则会导致方法调用突然完成。突然完成的方法调用永远不会向其调用者返回值。
2.7. 对象的表示
Java 虚拟机不强制要求对象有任何特定的内部结构。
在 Oracle 的 Java 虚拟机实现中,对类实例的引用是一个指向句柄的指针,该句柄本身就是一对指针:一个指向包含对象方法的表,另一个指向代表该对象的 Class对象。对象的类型,另一个是从堆中为对象数据分配的内存。
2.8. 浮点运算
Java 虚拟机包含 IEEE 754 标准 (JLS §1.7) 中指定的浮点算术子集。
在 Java SE 15 及更高版本中,Java 虚拟机使用 2019 版 IEEE 754 标准。在 Java SE 15 之前,Java 虚拟机使用 1985 版本的 IEEE 754 标准,其中 binary32 格式称为单格式,binary64 格式称为双格式。
许多用于算术(§2.11.3)和类型转换(§2.11.4)的 Java 虚拟机指令都使用浮点数。这些指令通常对应于 IEEE 754 操作(表 2.8-A),除了下面描述的某些指令之外。
表 2.8-A。与 IEEE 754 操作的对应关系
Java 虚拟机和 IEEE 754 标准支持的浮点运算之间的主要区别是:
浮点余数指令drem ( § drem ) 和fem ( § fem ) 不对应于 IEEE 754 余数运算。这些指令基于使用向零舍入策略舍入的隐含除法;相反,IEEE 754 余数基于使用舍入到最接近的舍入策略的隐含除法。(舍入政策将在下面讨论。)
浮点取反指令dneg ( § dneg ) 和fneg ( § fneg ) 并不精确对应 IEEE 754 取反操作。特别是,这些指令不需要反转 NaN 操作数的符号位。
Java 虚拟机的浮点指令不会引发异常、陷阱或以其他方式发出无效操作、除以零、上溢、下溢或不精确的 IEEE 754 异常情况信号。
Java 虚拟机不支持 IEEE 754 信号浮点比较,并且没有信号 NaN 值。
IEEE 754 包含与 Java 虚拟机中的舍入策略不对应的舍入方向属性。Java 虚拟机不提供任何方法来更改给定浮点指令使用的舍入策略。
Java 虚拟机不支持 IEEE 754 定义的二进制 32 扩展和二进制 64 扩展浮点格式。在操作或存储浮点值时,不得使用 超出float和类型指定的扩展范围和扩展精度。double
Math一些在 Java 虚拟机中没有相应指令的 IEEE 754 运算是通过和类中的方法提供的 StrictMath,包括sqrtIEEE 754 squareRoot 运算的方法、fmaIEEE 754 fusedMultiplyAdd 运算的方法和IEEEremainder IEEE 754 余数运算的方法。
Java 虚拟机需要支持 IEEE 754 次正规 浮点数和渐进下溢,这使得更容易证明特定数值算法的所需属性。
浮点运算是真实运算的近似。虽然实数的数量是无限的,但特定的浮点格式仅具有有限数量的值。在Java虚拟机中,舍入策略是用于将实数映射到给定格式的浮点值的函数。对于浮点格式可表示范围内的实数,实数轴的连续段被映射到单个浮点值。其值在数值上等于浮点值的实数被映射到该浮点值;例如,实数 1.5 以给定格式映射到浮点值 1.5。Java虚拟机定义了两种舍入策略,如下:
舍入到最接近的舍入策略适用于除 (i) 转换为整数值和 (ii) 余数之外的所有浮点指令。在舍入到最接近的舍入策略下,不精确的结果必须舍入到最接近无限精确结果的可表示值;如果两个最接近的可表示值同样接近,则选择最低有效位为零的值。
舍入到最接近的舍入策略对应于 IEEE 754 中二进制算术的默认舍入方向属性 roundTiesToEven。
roundTiesToEven舍 入方向属性在 1985 版 IEEE 754 标准中称为“舍入到最近”舍入模式。Java虚拟机中的舍入策略就是以这种舍入模式命名的。
向零舍入 策略适用于 (i) 通过d2i、d2l、f2i和f2l指令(§ d2i、§ d2l、 § f2i、§ f2l )将浮点值转换为整数值 ,以及 (ii ) 浮点余数指令drem和frem ( § drem , § frem )。根据零舍入政策,不精确的结果将舍入到最接近的可表示值,该值的大小不大于无限精确的结果。对于转换为整数,向零舍入策略的舍入相当于舍弃小数有效位的截断。
向零舍入策略的舍入对应于 IEEE 754 中二进制算术的roundTowardZero舍入方向属性。
roundTowardZero 舍入方向属性在 1985 版 IEEE 754 标准中称为“向零舍入”舍入模式。Java虚拟机中的舍入策略就是以这种舍入模式命名的。
Java 虚拟机要求每个浮点指令将其浮点结果舍入到结果精度。每条指令使用的舍入策略是舍入到最接近的值或舍入到零,如上所述。
Java 1.0 和 1.1 要求对浮点表达式进行严格的计算。严格求值意味着每个float 操作数对应于可以以 IEEE 754 二进制 32 格式表示的值,每个double操作数对应于可以以 IEEE 754 二进制 64 格式表示的值,并且具有相应 IEEE 754 操作的每个浮点运算符与 IEEE 754 结果匹配相同的操作数。
严格的评估提供了可预测的结果,但在 Java 1.0/1.1 时代常见的一些处理器系列的 Java 虚拟机实现中引起了性能问题。因此,在 Java 1.2 到 Java SE 16 中,Java SE 平台允许 Java 虚拟机实现具有与每个浮点类型关联的一个或两个值集。类型与浮点值集和 浮点扩展指数值集float关联 ,而 类型与双精度值集和双精度扩展指数值集关联。浮点值集对应于以 IEEE 754 二进制 32 格式表示的值;浮点扩展指数值集具有相同的精度位数,但指数范围更大。类似地,双精度值集对应于以 IEEE 754 二进制 64 格式表示的值;双扩展指数值集具有相同的精度位数,但指数范围更大。默认情况下允许使用扩展指数值集可以改善某些处理器系列上的性能问题。 double
为了兼容性,Java 1.2 允许class文件禁止实现使用扩展指数值集。文件通过在方法声明上class设置标志来表达这一点。限制方法指令的浮点语义,以使用操作数的浮点值集和操作数的双精度值集 ,确保此类指令的结果是完全可预测的。因此标记为的方法具有与 Java 1.0 和 1.1 中指定的相同的浮点语义。 ACC_STRICTACC_STRICTfloatdoubleACC_STRICT
在 Java SE 17 及更高版本中,Java SE 平台始终要求对浮点表达式进行严格计算。处理器系列的新成员在实施严格评估时遇到性能问题,但不再遇到这种困难。该规范不再将float和double与上述四个值集相关联,并且该ACC_STRICT标志不再影响浮点运算的评估。ACC_STRICT为了兼容性,在主版本号为 46-60 的文件中分配用于表示的位模式在主版本号大于 60 的文件class中未分配(即不表示任何标志) (第 4.6 节)。Java 虚拟机的未来版本可能会为未来文件中的位模式分配不同的含义。 classclass
2.9. 特殊方法
2.9.1. 实例初始化方法
一个类有零个或多个实例初始化方法,每个方法通常对应于用 Java 编程语言编写的构造函数。
如果满足以下所有条件,则该方法是实例初始化方法:
它是在类(而不是接口)中定义的。
它有一个特殊的名字<init>。
它是void(4.3.3)。
在类中,任何void名为的非方法<init>都不是实例初始化方法。在接口中,任何命名的方法<init>都不是实例初始化方法。此类方法不能由任何 Java 虚拟机指令(第 4.4.2 节、 第 4.9.2 节)调用,并且会被格式检查拒绝(第 4.6 节、第 4.8 节)。
实例初始化方法的声明和使用受到Java虚拟机的约束。对于声明,方法的 access_flags项和code数组受到约束(第 4.6 节、第 4.9.2 节)。对于使用,实例初始化方法只能由未初始化的类实例上的invokespecial指令调用 (第 4.10.1.9 节)。
由于该名称<init>在 Java 编程语言中不是有效的标识符,因此不能直接在用 Java 编程语言编写的程序中使用。
2.9.2. 类初始化方法
一个类或接口最多有一个类或接口初始化方法,并由调用该方法的 Java 虚拟机进行初始化(第 5.5 节)。
如果满足以下所有条件,则 方法是类或接口初始化方法:
它有一个特殊的名字<clinit>。
它是void(§4.3.3)。
在class版本号为 51.0 或更高版本的文件中,该方法设置了其ACC_STATIC标志并且不接受任何参数(第 4.6 节)。
Java SE 7 中引入了的要求ACC_STATIC,Java SE 9 中引入了不带参数的要求。在版本号为 50.0 或更低的类文件中,名为 的方法被视为 <clinit>类void或接口初始化方法,无论设置如何它的 ACC_STATIC标志或是否需要参数。
<clinit>文件中 命名的其他方法class不是类或接口初始化方法。它们永远不会被 Java 虚拟机本身调用,也不能被任何 Java 虚拟机指令(第 4.9.1 节)调用,并且会被格式检查拒绝(第 4.6 节、第 4.8 节)。
由于该名称<clinit>在 Java 编程语言中不是有效的标识符,因此不能直接在用 Java 编程语言编写的程序中使用。
2.9.3. 特征多态性方法
如果满足以下所有条件,则 方法是签名多态的:
它是在java.lang.invoke.MethodHandle类或java.lang.invoke.VarHandle类中声明的。
它有一个类型为 的形式参数Object[]。
它已设置ACC_VARARGS和ACC_NATIVE标志。
Java 虚拟机对invokevirtual指令(§invokevirtual)中的签名多态方法进行特殊处理,以便实现方法句柄的调用或 实现对 实例引用的变量的访问java.lang.invoke.VarHandle。
方法句柄是对底层方法、构造函数、字段或类似低级操作(第5.4.3.5 节)的动态强类型且可直接执行的引用,具有可选的参数或返回值转换。的实例 java.lang.invoke.VarHandle是对变量或变量族的动态强类型引用,包括static字段、非static字段、数组元素或堆外数据结构的组件。有关详细信息,请参阅java.lang.invokeJava SE 平台 API 中的包。
2.10. 例外情况
ThrowableJava 虚拟机中的异常由类或其子类之一的实例表示。引发异常会导致从引发异常的位置立即进行非本地控制转移。
大多数异常都是由于发生异常的线程的操作而同步发生的。相比之下,异步异常可能发生在程序执行过程中的任何时刻。Java 虚拟机因以下三个原因之一引发异常:
执行了 athrow指令( § athrow )。
Java虚拟机同步检测到异常执行情况。这些异常不会在程序中的任意点抛出,而是仅在执行以下指令后同步抛出:
将异常指定为可能的结果,例如:
当指令包含违反 Java 编程语言语义的操作时,例如在数组边界之外进行索引。
当加载或链接部分程序时发生错误时。
导致超出资源的某些限制,例如使用过多内存时。
发生异步异常是因为 Java 虚拟机实现中发生内部错误(第 6.3 节)。
Java 虚拟机实现可能允许在引发异步异常之前发生少量但有限的执行。允许这种延迟是为了允许优化的代码在可以实际处理这些异常的地方检测并抛出这些异常,同时遵守 Java 编程语言的语义。
一个简单的实现可能会在每个控制传输指令点轮询异步异常。由于程序的大小有限,因此这限制了检测异步异常的总延迟。由于控制传输之间不会发生异步异常,因此代码生成器具有一定的灵活性,可以在控制传输之间重新排序计算以获得更高的性能。Marc Feeley 的论文“对库存硬件进行有效轮询” , Proc。1993 年函数式编程和计算机体系结构会议,丹麦哥本哈根,第 179-187 页,建议进一步阅读。
Java 虚拟机抛出的异常是精确的:当发生控制权转移时,在抛出异常的点之前执行的指令的所有效果都必须看起来已经发生。在引发异常的点之后发生的指令可能看起来没有被评估。如果优化的代码已推测性地执行了异常发生点之后的某些指令,则此类代码必须准备好从程序的用户可见状态中隐藏此推测性执行。
Java 虚拟机中的每个方法都可能与零个或多个异常处理程序相关联。异常处理程序指定 Java 虚拟机代码中实现异常处理程序处于活动状态的方法的偏移量范围,描述异常处理程序能够处理的异常类型,并指定要处理的代码的位置那个例外。如果引起异常的指令的偏移量在异常处理程序的偏移量范围内,并且异常类型与异常处理程序处理的异常类是同一类或其子类,则异常与异常处理程序匹配。当抛出异常时,Java虚拟机在当前方法中搜索匹配的异常处理程序。如果找到匹配的异常处理程序,系统将分支到匹配的处理程序指定的异常处理代码。
如果在当前方法中没有找到这样的异常处理程序,则当前方法调用突然完成(§2.6.5)。突然完成时,当前方法调用的操作数堆栈和局部变量将被丢弃,并且其帧将被弹出,恢复调用方法的帧。然后,在调用者框架的上下文中重新引发异常,依此类推,继续沿方法调用链向上。如果在到达方法调用链的顶部之前没有找到合适的异常处理程序,则抛出异常的线程的执行将被终止。在线程终止之前,未捕获的异常按照以下规则进行处理:
如果线程设置了未捕获的异常处理程序,则执行该处理程序。
否则,将为该线程的父线程uncaughtException调用 该方法。ThreadGroup如果 ThreadGroup及其父级ThreadGroup不 override ,则调用 uncaughtException默认处理程序的 方法。uncaughtException
搜索方法的异常处理程序以查找匹配项的顺序很重要。在class文件中,每个方法的异常处理程序都存储在表中(第 4.7.3 节)。在运行时,当抛出异常时,Java 虚拟机按照文件中相应异常处理程序表中出现的顺序搜索当前方法的异常处理程序,从该表的开头开始class。
请注意,Java 虚拟机不强制方法的异常表条目的嵌套或任何顺序。Java 编程语言的异常处理语义只能通过与编译器的配合来实现(第 3.12 节)。当class通过其他方式生成文件时,定义的搜索过程可确保所有 Java 虚拟机实现的行为一致。