类文件格式
编译后的代码以硬件和操作系统无关的二进制格式表示,通常(但不一定)存储在文件中,这种格式被称为类文件格式。类文件格式精确地定义了类或接口的表示方式,包括在特定平台的对象文件格式中可能被忽略的细节,例如字节顺序。 后面会”详细介绍了类文件格式。
数据类型
像Java编程语言一样,Java虚拟机操作两种类型的:原始类型和引用类型。相应地,有两种值可以存储在变量中,作为参数传递,由方法返回,并进行操作:原始值和引用值。
Java虚拟机期望几乎所有的类型检查都在运行时之前完成,通常由编译器完成,而不需要由Java虚拟机本身完成。原始类型的值在运行时不需要被标记或以其他方式检查以确定其类型,也不需要与引用类型的值区分开来。相反,Java虚拟机的指令集通过为特定类型的值操作设计的指令来区分其操作数类型。例如,iadd、ladd、fadd和dadd都是Java虚拟机的指令,它们将两个数值相加并产生数值结果,但每个指令都针对其操作数类型进行了专门化:分别为int、long、float和double。
Java虚拟机包含对对象的显式支持。对象要么是动态分配的类实例,要么是数组。对对象的引用被视为具有Java虚拟机类型的引用。可以将类型引用的值视为指向对象的指针。对一个对象可能存在多个引用。对象总是通过类型引用的值进行操作、传递和测试。
原始类型和值
Java虚拟机支持的原始数据类型包括数值类型、布尔类型和返回地址类型。数值类型包括整数类型和浮点数类型。
整数类型:
- byte,其值为8位带符号的二进制补码整数,默认值为零
- short,其值为16位带符号的二进制补码整数,默认值为零
- int,其值为32位带符号的二进制补码整数,默认值为零
- long,其值为64位带符号的二进制补码整数,默认值为零
- char,其值为16位无符号整数,表示基本多语言平面中的Unicode代码点,采用UTF-16编码,默认值为零代码点('\u0000')
浮点类型:
- float,其值为浮点值集合的元素,或者在支持的情况下为扩展指数浮点值集合的元素,默认值为正零
- double,其值为双精度值集合的元素,或者在支持的情况下为扩展指数双精度值集合的元素,默认值为正零
布尔类型的值编码了真值true和false,默认值为false。返回地址类型的值是指向Java虚拟机指令的操作码的指针。在基本类型中,只有返回地址类型没有直接与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(包括)
浮点类型的值集和值
浮点类型为 float 和 double,它们在概念上与 32 位单精度和 64 位双精度格式的 IEEE 754 值及操作相关联,这些内容在《IEEE 二进制浮点算术标准》(ANSI/IEEE 标准 754-1985,New York)中有规定。
IEEE 754 标准不仅包括正负符号数值,还包括正负零、正负无穷大,以及一个特殊的非数字值(以下简称为“NaN”)。 NaN 值用于表示某些无效操作的结果,例如零除以零。
每个Java虚拟机的实现都必须支持两种标准的浮点数值集合,称为float值集和double值集。此外,Java虚拟机的实现可以选择支持两种扩展指数浮点数值集合中的一个或两个,称为float-extended-exponent值集和double-extended-exponent值集。在某些情况下,这些扩展指数值集可能会代替标准值集来表示float或double类型的值。
任何浮点值集的有限非零值都可以表示为 s ⋅ m ⋅ 2(e - N + 1) 的形式,其中 s 为 +1 或 -1,m 是一个小于 2N 的正整数,e 是介于 Emin = -(2K-1-2) 和 Emax = 2K-1-1(包括两端点)之间的整数,而 N 和 K 是取决于值集的参数。某些值可以用这种方式表示为多种形式;例如,假设值集中的某个值 v 可以使用特定的 s、m 和 e 值来表示,如果此时 m 为偶数且 e 小于 2K-1,则可以将 m 减半并将 e 增加 1,从而为相同的值 v 生成第二种表示方式。如果在该形式下的表示满足 m ≥ 2N-1,则称其为规范化的;否则称为非规范化的。如果值集中的某个值无法表示为使 m ≥ 2N-1 的形式,则称该值为非规范化值,因为它没有规范化的表示形式。
参数N和K的约束(以及派生参数Emin和Emax的约束)针对两个必需的和两个可选的浮点数值集汇总在下表。
如果某个实现支持一个或两个扩展指数值集,则对于每个受支持的扩展指数值集,都存在一个特定的、依赖于实现的常量 K,其值受限于表 2.3.2-A;此值 K 进而决定了 Emin 和 Emax 的值。
每个四个值集不仅包括上述的有限非零值,还包括五个值:正零、负零、正无穷大、负无穷大和 NaN。
表2.3.2-A中的约束条件设计的目的是,使得浮点数值集中的每个元素必然也是浮点扩展指数数值集、双精度数值集以及双精度扩展指数数值集的元素。同样地,双精度数值集的每个元素必然也是双精度扩展指数数值集的元素。每个扩展指数数值集相比对应的標準数值集具有更大的指数范围,但并未提高精度。
浮点数值集的元素正是可以在IEEE 754标准中定义的单精度浮点格式下表示的值,不同之处在于只有一个NaN值(IEEE 754规定了224-2个不同的NaN值)。双精度数值集的元素则是在IEEE 754标准中定义的双精度浮点格式下可以表示的值,不同之处同样在于只有一个NaN值(IEEE 754规定了253-2个不同的NaN值)。然而,请注意,这里定义的扩展指数浮点和扩展指数双精度数值集的元素并不对应于IEEE 754单精度扩展和双精度扩展格式下可以表示的值。本规范并未强制要求对浮点数值集的具体表示方式,除非浮点值必须在类文件格式中表示。
浮点数、扩展指数浮点数、双精度数和扩展指数双精度数值集并不是类型。对于Java虚拟机的实现,使用浮点数值集的一个元素来表示浮点类型的值始终是正确的;然而,在某些上下文中,实现可能也可以使用扩展指数浮点数值集的一个元素来代替。同样地,使用双精度数值集的一个元素来表示双精度类型的值始终是正确的;然而,在某些上下文中,实现可能也可以使用扩展指数双精度数值集的一个元素来代替。
除了NaN之外,浮点数值集的值是有序的。从小到大排列时,它们是负无穷大、负有限值、正负零、正有限值和正无穷大。
浮点正零和浮点负零比较时相等,但有其他操作可以区分它们;例如,1.0 除以 0.0 会产生正无穷大,而 1.0 除以 -0.0 则会产生负无穷大。 NaN 是无序的,因此数值比较和数值相等测试在任一或两个操作数为 NaN 时结果为假。特别是,一个值与自身的数值相等测试结果为假当且仅当该值为 NaN。如果任一操作数为 NaN,则数值不相等测试结果为真。
返回地址类型和值
返回地址类型被Java虚拟机的jsr、ret和jsr_w指令(§jsr,§ret,§jsr_w)使用。返回地址类型的值是指向Java虚拟机指令的操作码的指针。与数值基本类型不同,返回地址类型不对应于任何Java编程语言类型,并且不能被正在运行的程序修改。
boolean类型
尽管Java虚拟机定义了布尔类型,但它对该类型的支持非常有限。Java虚拟机没有专门用于布尔值操作的指令。相反,Java编程语言中对布尔值进行操作的表达式会被编译为使用Java虚拟机的int数据类型值。
Java虚拟机直接支持布尔数组。其newarray指令(§newarray)可以创建布尔数组。类型为boolean的数组使用字节数组指令baload和bastore(§baload,§bastore)进行访问和修改。
在Oracle的Java虚拟机实现中,Java编程语言中的布尔数组被编码为Java虚拟机字节数组,每个布尔元素使用8位。
Java虚拟机使用1表示true,0表示false来编码布尔数组组件。当Java编程语言中的布尔值被编译器映射到Java虚拟机的int类型值时,编译器必须使用相同的编码。
引用类型和值
有三种引用类型:类类型、数组类型和接口类型。它们的值分别是动态创建的类实例、数组或实现接口的类实例或数组的引用。
数组类型由单个维度的组件类型组成(其长度不由类型给出)。数组类型的组件类型本身也可以是数组类型。如果从任何数组类型开始,考虑其组件类型,然后(如果是数组类型)该类型的组件类型,依此类推,最终必须达到不是数组类型的组件类型;这被称为数组类型的元素类型。数组类型的元素类型必然是原始类型、类类型或接口类型之一。
一个引用值也可能是特殊的 null 引用,即对任何对象的引用,这里将用 null 表示。null 引用最初没有运行时类型,但可以转换为任何类型。引用类型的默认值是 null。本规范不要求具体的值编码 null。
运行时数据区
Java虚拟机定义了各种运行时数据区,这些数据区在程序执行期间使用。其中一些数据区在Java虚拟机启动时创建,并且仅在Java虚拟机退出时销毁。其他数据区是每个线程的。每个线程的数据区在线程创建时生成,并在线程退出时销毁。
程序计数器
Java虚拟机可以同时支持许多线程的执行。每个Java虚拟机线程都有自己的pc(程序计数器)寄存器。在任何时刻,每个Java虚拟机线程都在执行单个方法的代码,即该线程的当前方法。如果该方法不是本地方法,pc寄存器包含当前正在执行的Java虚拟机指令的地址。如果线程当前执行的方法是本地方法,则Java虚拟机的pc寄存器的值是未定义的。Java虚拟机的pc寄存器足够宽,可以容纳特定平台上的returnAddress或本地指针。
虚拟机栈
每个Java虚拟机线程都有一个私有的Java虚拟机栈,该栈与线程同时创建。Java虚拟机栈存储帧。Java虚拟机栈类似于C等传统语言的栈:它保存局部变量和中间结果,并在方法调用和返回中起作用。由于Java虚拟机栈除了压入和弹出帧外永远不会被直接操作,因此帧可以分配在堆上。Java虚拟机栈的内存不需要是连续的。
该规范允许Java虚拟机栈要么是固定大小,要么根据计算需求动态扩展和收缩。如果Java虚拟机栈是固定大小,则在创建每个栈时可以独立选择其大小。
Java虚拟机的实现可能会为程序员或用户提供对Java虚拟机堆栈初始大小的控制,同时,在Java虚拟机堆栈动态扩展或收缩的情况下,还可能提供对最大和最小大小的控制。
以下异常情况与Java虚拟机栈相关:
- 如果线程中的计算需要的Java虚拟机栈大小超过了允许的范围,Java虚拟机将抛出StackOverflowError。
- 如果Java虚拟机栈可以动态扩展,但在尝试扩展时无法分配足够的内存来实现扩展,或者如果无法分配足够的内存来为新线程创建初始Java虚拟机栈,Java虚拟机将抛出OutOfMemoryError。
堆
Java虚拟机有一个堆,它在所有Java虚拟机线程之间共享。堆是运行时数据区域,从中为所有类实例和数组分配内存。
堆在虚拟机启动时创建。用于对象的堆存储由自动存储管理系统(称为垃圾收集器)回收;对象永远不会被显式释放。Java虚拟机不假定任何特定类型的自动存储管理系统,存储管理技术可以根据实现者的需求进行选择。堆可以是固定大小的,也可以根据计算需求进行扩展,并且如果更大的堆变得不必要,它可以收缩。堆的内存不需要是连续的。
Java虚拟机实现可能会为程序员或用户提供对堆的初始大小的控制,此外,如果堆可以动态扩展或收缩,则还提供对最大和最小堆大小的控制。
以下异常情况与堆相关:
- 如果计算所需的堆内存超出了自动存储管理系统能够提供的范围,Java虚拟机会抛出 OutOfMemoryError
方法区
Java虚拟机有一个被所有Java虚拟机线程共享的方法区。方法区类似于传统语言的编译代码存储区,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括在类和实例初始化以及接口初始化中使用的特殊方法。
方法区在虚拟机启动时创建。尽管方法区在逻辑上是堆的一部分,简单的实现可以选择不对它进行垃圾回收或紧凑。本规范不强制规定方法区的位置或管理编译代码的策略。方法区可以是固定大小的,也可以根据计算需求进行扩展,并且如果需要更大的方法区,则可以收缩。方法区的内存不需要是连续的。
Java虚拟机实现可以为程序员或用户提供对方法区初始大小的控制,此外,在方法区大小可变的情况下,还可以控制方法区的最大和最小大小。
以下异常情况与方法区相关:
- 如果方法区中的内存无法满足分配请求,Java虚拟机会抛出OutOfMemoryError。
运行时常量池
运行时常量池是类文件中constant_pool表的每类或每接口的运行时表现形式。它包含多种常量,范围从编译时已知的数值字面量到必须在运行时解析的方法和字段引用。运行时常量池的功能类似于传统编程语言中的符号表,尽管它包含的数据范围比典型的符号表更广。
每个运行时常量池都从Java虚拟机的方法区分配。当Java虚拟机创建一个类或接口时,会构建该类或接口的运行时常量池。
以下异常情况与为类或接口构建运行时常量池相关:
- 在创建类或接口时,如果构建运行时常量池所需的内存超出了Java虚拟机方法区可提供的内存,Java虚拟机会抛出OutOfMemoryError。
本地方法栈
Java虚拟机的实现可能会使用传统的栈(通常称为“C栈”)来支持本地方法(用Java编程语言之外的其他语言编写的函数)。这些本地方法栈也可能被用于实现在C等语言中解释Java虚拟机指令集的解释器。如果一个Java虚拟机实现不支持加载本地方法,并且自身也不依赖传统栈,则不需要提供本地方法栈。如果提供了本地方法栈,它们通常会在每个线程创建时按线程分配。
本规范允许本地方法栈要么是固定大小,要么根据计算需求动态扩展和收缩。如果本地方法栈是固定大小的,则在创建该栈时可以独立选择每个本地方法栈的大小。
Java虚拟机实现可能会为程序员或用户提供对本地方法栈初始大小的控制,同时,在本地方法栈大小可变的情况下,还提供对方法栈最大和最小大小的控制。
以下异常情况与本地方法栈相关:
- 如果线程中的计算所需的本地方法栈超出了允许的大小,Java虚拟机将抛出StackOverflowError。
- 如果本地方法栈可以动态扩展,在尝试扩展时如果无法分配足够的内存,或者如果无法为新线程创建初始本地方法栈分配足够的内存,Java虚拟机将抛出OutOfMemoryError。
帧
帧用于存储数据和部分结果,执行动态链接、返回方法值以及分发异常。
每次调用方法时都会创建一个新的帧。当方法调用完成时,无论是正常完成还是突然完成(它抛出一个未捕获的异常),帧就会被销毁。帧从创建该帧的线程的 Java 虚拟机栈中分配。每个帧都有自己的局部变量数组、自己的操作数栈,并且有一个对当前方法类的运行时常量池的引用。
一个框架可以通过添加特定于实现的附加信息进行扩展,例如调试信息。
局部变量数组和操作数栈的大小在编译时确定,并与方法相关联的代码一起提供。因此,帧数据结构的大小仅取决于Java虚拟机的实现,这些结构的内存可以在方法调用时同时分配。
在给定的控制线程中,任何时刻只有一个帧是活跃的,那就是执行方法的帧。这个帧被称为当前帧,其对应的方法被称为当前方法。定义当前方法的类称为当前类。对局部变量和操作数栈的操作通常都是针对当前帧进行的。
当一个方法调用另一个方法或该方法完成时,当前帧将不再保持为当前状态。当调用一个方法时,会创建一个新的帧,并在控制转移到新方法时成为当前帧。在方法返回时,当前帧将其方法调用的结果(如果有)传递回上一个帧。然后当前帧被丢弃,上一个帧重新成为当前帧。
请注意,由线程创建的帧仅对该线程本地可见,不能被其他线程引用。
局部变量
每一帧包含一个被称为局部变量的变量数组。帧的局部变量数组的长度在编译时确定,并与该帧相关联的方法的代码一起提供在类或接口的二进制表示中。
一个单独的局部变量可以保存类型为布尔值、字节、字符、短整型、整型、 浮点型、引用或返回地址的值。一对局部变量可以保存类型为长整型或双精度浮点型的值。
局部变量通过索引访问。第一个局部变量的索引为零。当且仅当一个整数在零和局部变量数组大小减一之间时,该整数被视为局部变量数组的索引。
类型为long或double的值占用两个连续的局部变量。 此类值只能使用较小的索引来寻址。例如,存储在局部变量数组索引n处的double类型值实际上占用了索引n和n+1的局部变量; 然而,无法从索引n+1处的局部变量加载。但是,可以存储到其中。不过,这样做会使索引n处局部变量的内容失效。
Java虚拟机不要求n为偶数。直观来说,long和double类型的值不必在局部变量数组中64位对齐。实现者可以自由决定使用为该值保留的两个局部变量来表示此类值的适当方法。
Java虚拟机使用局部变量在方法调用时传递参数。在类方法调用时,任何参数都会从局部变量0开始依次传递到连续的局部变量中。在实例方法调用时,局部变量0始终用于传递正在调用该实例方法的对象的引用(在Java编程语言中为“this”)。随后,任何参数都从局部变量1开始依次传递到连续的局部变量中。
操作数栈
每个帧包含一个后进先出(LIFO)的栈,称为操作数栈。帧的操作数栈的最大深度在编译时确定,并与该帧相关联的方法代码一起提供。
在上下文明确的情况下,我们有时会将当前帧的操作数栈简单地称为操作数栈
操作数栈在包含它的帧被创建时为空。Java虚拟机提供指令将常量或局部变量或字段的值加载到操作数栈上。其他Java虚拟机指令从操作数栈中获取操作数,对它们进行操作,并将结果推回操作数栈。操作数栈还用于准备要传递给方法的参数以及接收方法结果。
例如,iadd指令(§iadd)将两个int值相加。它要求要相加的int值是操作数栈的顶部两个值,由之前的指令推送至此。两个int值都会从操作数栈中弹出,然后进行相加,其和会被推回操作数栈。子计算可以在操作数栈上嵌套,从而生成可供外围计算使用的值。
操作数栈中的每个条目都可以保存任何 Java 虚拟机类型的值, 包括类型为 long 或类型为 double 的值。
操作数栈上的值必须以适合其类型的方式进行操作。例如,不可能推送两个 int 值然后随后将它们视为一个 long 值,或者推送两个 float 值然后随后用 iadd 指令将它们相加。少量的 Java 虚拟机指令(dup 指令(§dup)和 swap(§swap))以原始值的方式对运行时数据区域进行操作而不考虑其具体类型;这些指令被定义为无法用于修改或拆分单个值。这些对操作数栈操作的限制通过类文件验证来强制执行。 在任何时候,操作数栈都有一个相关的深度,其中类型为 long 或 double 的值对深度贡献两个单位,而任何其他类型的值贡献一个单位。
动态链接
每一帧包含对当前方法类型的运行时常量池(§2.5.5)的引用,以支持方法代码的动态链接。 方法的类文件代码通过符号引用引用要调用的方法和要访问的变量。动态链接将这些符号方法引用转换为具体的方法引用,根据需要加载类以解析尚未定义的符号,并将变量访问转换为与这些变量的运行时位置相关的存储结构中的适当偏移量。 这种方法和变量的后期绑定,使得该方法所使用的其他类中的更改不太可能破坏此代码。
普通方法调用完成
如果调用没有导致异常被抛出,无论是直接来自Java虚拟机还是作为执行显式throw语句的结果,则方法调用将正常完成。如果当前方法的调用正常完成,则可以将值返回给调用方法。当被调用的方法执行其中一个return指令时会发生这种情况,所选指令必须适合返回值的类型(如果有)。
当前帧(在这种情况下用于恢复调用者的状态, 包括其局部变量和操作数栈,并将调用者的程序计数器适当地递增,以跳过方法调用指令。 然后,调用方法的帧继续正常执行,返回值(如果有)会被推入该帧的操作数栈中。
突然的方法调用完成
如果方法内的 Java 虚拟机指令执行导致 Java 虚拟机抛出异常并且该异常未在方法内处理,则方法调用会突然中止。athrow 指令(§athrow)的执行也会显式地抛出异常,如果该异常未被当前方法捕获,则会导致方法调用突然中止。突然中止的方法调用永远不会向其调用者返回值。
对象的表示
Java虚拟机不要求任何特定的对象内部结构。 在Oracle的某些Java虚拟机实现中,对类实例的引用是一个指向句柄的指针,该句柄本身是一对指针:一个指向包含对象方法的表和表示对象类型的Class对象,另一个指向从堆中分配用于存储对象数据的内存。
浮点运算
Java虚拟机包含了IEEE二进制浮点算术标准(ANSI/IEEE Std. 754-1985,New York)中指定的一部分浮点运算。
Java虚拟机浮点算术与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扩展格式要求扩展精度以及扩展指数范围。
浮点模式
每种方法都有一个浮点数模式,该模式要么是FP-strict(严格浮点),要么不是FP-strict。方法的浮点数模式由定义该方法的方法信息结构中的访问标志项的ACC_STRICT标志的设置决定。如果设置了此标志,则该方法为FP-strict;否则,该方法不是FP-strict。 请注意,这种ACC_STRICT标志的映射意味着在JDK 1.1或更早版本的编译器中编译的类中的方法实际上并非FP-strict。
当我们提到操作数栈具有特定的浮点数模式时,是指创建包含该操作数栈的帧的方法调用具有该浮点数模式。同样地,当我们提到Java虚拟机指令具有特定的浮点数模式时,是指包含该指令的方法具有该浮点数模式。
如果支持浮点扩展指数值集则操作数栈上类型为 float 的值(如果不是 FP-strict)可以在该值集范围内取值,除非被值集转换禁止。如果支持双精度扩展指数值集,则操作数栈上类型为 double 的值(如果不是 FP-strict)可以在该值集范围内取值,除非被值集转换禁止。
在所有其他上下文中,无论是在操作数栈上还是其他地方,无论浮点模式如何,类型为 float 和 double 的浮点值只能分别在 float 值集和 double 值集范围内取值。特别是,类和实例字段、数组元素、局部变量和方法参数只能包含来自标准值集的值。
值集转换
一个支持扩展浮点值集的Java虚拟机实现,在指定情况下,被允许或要求将相关浮点类型的值在扩展值集和标准值集之间进行映射。这样的值集转换不是类型转换。
在需要进行值集转换的地方,允许实现对值执行以下操作之一:
- 如果该值的类型为 float 且不是 float 值集的元素,则将其映射到 float 值集的最近元素。
- 如果该值的类型为 double 且不是 double 值集的元素,则将其映射到 double 值集的最近元素。
此外,在需要进行值集转换的地方,要求执行某些操作:
- 假设执行一条非 FP-strict 的 Java 虚拟机指令,导致一个类型为 float 的值被推送到 FP-strict 的操作数栈、作为参数传递、或存储到本地变量、字段或数组元素中。如果该值不是 float 值集的元素,则将其映射到 float 值集的最近元素。
- 假设执行一条非FP-strict的Java虚拟机指令,导致一个double类型的值被推送到一个FP-strict的操作数栈中,作为参数传递,或者存储到一个本地变量、字段或数组元素中。如果该值不是double值集的元素,则将其映射到double值集中最近的元素。
这种所需的价值集转换可能发生在方法调用期间传递浮点类型的参数时,包括本地方法调用;从不是FP-strict的方法返回浮点类型的值到是FP-strict的方法;或者在不是FP-strict的方法中将浮点类型的值存储到局部变量、字段或数组中。 并非所有来自扩展指数值集的值都能被精确映射到相应标准值集中的值。如果被映射的值太大而无法被精确表示(其指数大于标准值集所允许的范围),它将被转换为相应类型的(正或负)无穷大。如果被映射的值太小而无法被精确表示(其指数小于标准值集所允许的范围),它将被四舍五入到最近的可表示非规范化值或相同符号的零。
值集转换保留无穷大和NaN,并且不会改变被转换值的符号。值集转换对非浮点类型的值没有影响。
特殊方法
在Java虚拟机层面,用Java编程语言编写的每个构造函数(JLS §8.8)都表现为一个实例初始化方法,该方法具有特殊的名称< init>。这个名字由编译器提供。由于名称< init>不是一个有效的标识符,因此不能在用Java编程语言编写的程序中直接使用。实例初始化方法只能在Java虚拟机内部通过invokespecial指令(§invokespecial)调用,并且只能在未初始化的类实例上调用。实例初始化方法继承了生成它的构造函数的访问权限(JLS §6.6)。
一个类或接口最多有一个类或接口初始化方法,并通过调用该方法进行初始化。 类或接口的初始化方法具有特殊的名称< clinit>,不接受任何参数,并且是void类型。
类文件中其他名为< clinit>的方法无关紧要。它们不是类或接口初始化方法。无法通过任何Java虚拟机指令调用它们,并且永远不会被Java虚拟机本身调用。
在一个版本号为51.0或更高的类文件中,该方法还必须设置其ACC_STATIC标志,才能成为类或接口的初始化方法。
此要求是在Java SE 7中引入的。在版本号为50.0或更低的类文件中,名为< clinit>的方法如果为void且不带参数,则被视为类或接口的初始化方法,而不论其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包。
异常
Java虚拟机中的异常由类Throwable或其子类之一的实例表示。抛出异常会导致从异常被抛出的位置立即进行非本地控制转移。
大多数异常是由于发生异常的线程中的某个操作而同步发生的。相比之下,异步异常可能发生在程序执行的任何位置。Java虚拟机出于以下三种原因之一抛出异常。
- 执行了 athrow 指令(§athrow)。
- Java 虚拟机同步检测到一个异常执行条件。这些异常不是在程序的任意点抛出的,而是在以下指令执行后同步抛出的:
指令指定了异常作为可能的结果,例如:- 当指令包含违反 Java 编程语言语义的操作时,例如数组索引超出边界。
- 当程序加载或链接部分发生错误时。
- 导致某种资源限制被超过,例如使用过多内存。
- 发生了异步异常,因为:
- 类 Thread 或 ThreadGroup 的 stop 方法被调用,或者
- Java 虚拟机实现中发生了内部错误。
停止方法可能由一个线程调用,以影响另一个线程或指定线程组中的所有线程。它们是异步的,因为它们可能发生在其他线程执行过程中的任何点。内部错误被视为异步
Java虚拟机可能会允许在抛出异步异常之前发生少量但有限的执行。这种延迟是为了让优化后的代码能够在实际处理这些异常的同时,遵循Java编程语言的语义。
Java 虚拟机抛出的异常是精确的:当控制权转移时,必须看起来在抛出异常的点之前执行的指令的所有效果都已发生。在抛出异常的点之后出现的指令不能看起来已被评估。如果经过优化的代码已推测性地执行了异常发生点之后的一些指令,那么此类代码必须准备好将这种推测性执行隐藏在用户可见的程序状态之外。
Java 虚拟机中的每个方法都可以关联零个或多个异常处理程序。异常处理程序指定了其处于活动状态的方法实现的 Java 虚拟机代码中的偏移量范围,描述了该异常处理程序能够处理的异常类型,并指定了处理该异常的代码的位置。如果异常发生指令的偏移量在异常处理程序的偏移量范围内,并且异常与异常处理程序所描述的异常类型匹配,则异常与异常处理程序匹配。类型与异常处理程序所处理的异常类属于同一类或为其子类。当抛出异常时,Java 虚拟机会在当前方法中搜索匹配的异常处理程序。如果找到匹配的异常处理程序,系统将跳转到该匹配处理程序指定的异常处理代码。
如果在当前方法中未找到这样的异常处理程序,则当前方法调用会突然终止。在突然终止时,当前方法调用的运算栈和局部变量将被丢弃,其帧将被弹出,恢复调用方法的帧。然后抛出异常。
《JAVA 虚拟机结构》指令集概要
重新抛出异常时,会将其置于调用方的帧中,依此类推,沿着方法调用链向上继续。如果在到达方法调用链的顶端之前未找到合适的异常处理程序,则会终止抛出异常的线程的执行。
方法的异常处理程序的搜索匹配顺序非常重要。在类文件中,每个方法的异常处理程序都存储在一个表中。在运行时,当抛出异常时,Java 虚拟机会按照类文件中相应异常处理程序表中出现的顺序搜索当前方法的异常处理程序,从该表的开头开始。
请注意,Java 虚拟机不会强制方法的异常表条目进行嵌套或遵循任何特定顺序。Java 编程语言的异常处理语义仅通过与编译器的协作来实现。当通过其他方式生成类文件时,定义的搜索过程可确保所有 Java 虚拟机实现的行为保持一致。
指令集摘要
一条Java虚拟机指令由一个字节的操作码组成,该操作码指定要执行的操作,后面跟着零个或多个操作数,提供操作使用的参数或数据。许多指令没有操作数,仅由操作码组成。
忽略异常情况,Java虚拟机解释器的内循环实际上是
do {
atomically calculate pc and fetch opcode at pc;
if (operands) fetch operands;
execute the action for the opcode;
} while (there is more to do);
操作数的数量和大小由操作码确定。如果操作数的大小超过一个字节,则它以大端顺序存储——高位字节在前。例如,一个无符号的16位本地变量索引被存储为两个无符号字节,byte1和byte2,其值为 (byte1 << 8) | byte2。
字节码指令流仅单字节对齐。两个例外是 lookupswitch 和 tableswitch 指令(§lookupswitch,§tableswitch),它们会被填充以强制将某些操作数对齐到4字节边界。
将Java虚拟机操作码限制为一个字节的决定,以及在编译代码中放弃数据对齐,反映了对紧凑性的有意偏向,这可能会以牺牲某些原始实现的性能为代价。一个字节的操作码还限制了指令集的大小。不假设数据对齐意味着,在许多机器上,运行时必须从字节构建大于一个字节的立即数据。
类型和Java虚拟机
Java 虚拟机指令集中的大多数指令都对其执行的操作的类型信息进行了编码。例如,iload 指令(§iload)将一个局部变量的内容(该变量必须是 int 类型)加载到操作数栈上。fload 指令(§fload)则以同样的方式加载一个 float 值。这两个指令可能具有相同的实现,但具有不同的操作码。对于大多数有类型的指令,指令类型在操作码助记符中通过一个字母明确表示:i 表示 int 操作,l 表示 long,s 表示 short,b 表示 byte,c 表示 char,f 表示 float,d 表示 double,a 表示引用。对于类型明确的某些指令,其助记符中没有类型字母。例如,arraylength 总是操作一个数组对象。某些指令,如 goto(无条件控制转移),并不操作有类型的操作数。
鉴于 Java 虚拟机的操作码大小为一个字节,将类型编码到操作码中给其指令集的设计带来了压力。如果每个有类型的指令都支持 Java 虚拟机的所有运行时数据类型,那么就会有Java 虚拟机的指令集包含的指令多到无法用一个字节来表示。因此,Java 虚拟机的指令集对某些操作提供的类型支持有所减少。换句话说,指令集并非完全正交。必要时,可以使用单独的指令在不受支持的数据类型和受支持的数据类型之间进行转换。
表 2.11.1-A 总结了 Java 虚拟机指令集中的类型支持情况。通过将操作码列中指令模板中的 T 替换为类型列中的字母,即可构建出带有类型信息的特定指令。如果某些指令模板和类型的类型列为空,则表示不存在支持该类型操作的指令。例如,存在用于类型 int 的加载指令 iload,但不存在用于类型 byte 的加载指令。
请注意,表 2.11.1-A 中的大多数指令都没有 byte、char 和 short 这些整型的格式。也没有布尔类型的格式。编译器使用 Java 虚拟机指令集对类型为 byte 和 short 的字面值加载进行编码。THE STRUCTURE OF THE JAVA VIRTUAL MACHINE 指令集摘要 2.127
在编译时或运行时将这些值扩展为 int 类型值的指令。布尔型和字符型字面值的加载使用在编译时或运行时将字面值零扩展为 int 类型值的指令进行编码。同样,从布尔型、字节型、短整型和字符型数组加载值时,使用 Java 虚拟机指令将这些值扩展为 int 类型。因此,大多数对实际类型为布尔型、字节型、字符型和短整型的值的操作,都是通过操作计算类型为 int 的值的指令来正确执行的。
Java虚拟机实际类型与Java虚拟机计算类型的映射关系由表2.11.1-B总结。
某些Java虚拟机指令(例如pop和swap)在操作数栈上进行操作时不受类型的约束; 然而,这些指令的使用仅限于特定类别的计算类型值,同样见表中给出的内容。
加载和存储指令
加载和存储指令在 Java 虚拟机帧(§2.6)的局部变量(§2.6.1)和操作数栈(§2.6.2)之间传输值:
- 将局部变量加载到操作数栈:iload、iload_< n>、lload、lload_< n>、fload、fload_< n>、dload、dload_< n>、aload、aload_< n>。
- 将操作数栈中的值存储到局部变量:istore、istore_、lstore、lstore_、fstore、fstore_< n>、dstore、dstore_< n>、astore、astore_< n>。
- 将常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_< i>、lconst_< l>、fconst_< f>、dconst_< d>。
- 使用更宽的索引访问更多局部变量,或使用更大的立即操作数:wide。
访问对象字段和数组元素(§2.11.5)的指令也与操作数栈进行数据传输。指令集概要 THE STRUCTURE OF THE JAVA VIRTUAL MACHINE30.
上面所示带有尖括号中尾随字母的指令助记符(例如,iload_< n>)表示指令系列(以 iload_ 为例,其成员为 iload_0、iload_1、iload_2 和 iload_3)。此类指令系列是额外通用指令(iload)的特例,通用指令带有一个操作数。对于特例指令,操作数是隐式的,无需存储或获取。语义上是相同的(iload_0 与操作数为 0 的 iload 意义相同)。尖括号中的字母指定了该指令系列隐式操作数的类型:对于 < n>,为非负整数;对于 < i>,为 int;对于 < l>,为 long;对于 < f>,为 float;对于 < d>,为 double。在许多情况下,用于 int 类型的形式也用于对 byte、char 和 short 类型的值执行操作。 这种表示指令系列的符号在本规范中随处可见。
算数指令
算术指令会计算出一个结果,该结果通常是操作数栈上两个值的函数值,并将该结果推回到操作数栈中。主要有两种类型的算术指令:一种是操作整数值的指令,另一种是操作浮点数值的指令。在每种类型中,算术指令都针对 Java 虚拟机的数值类型进行了专门化处理。对于字节、短整型和字符类型的值,没有直接支持整数算术运算,对于布尔类型的值也是如此;这些操作由操作 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、lushr。
- 位或:ior、lor。
- 位与:iand、land。
- 位异或:ixor、lxor。
- 局部变量递增:iinc。
- 比较:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。
Java 编程语言中整型和浮点型值的运算符的语义(JLS 第 4.2.2 节、JLS 第 4.2.4 节)直接由 Java 虚拟机指令集的语义所支持。
Java 虚拟机在对整型数据类型进行操作时不会指示溢出。唯一可能导致异常的整型操作是整型除法指令(idiv 和 ldiv)以及整型余数指令(irem 和 lrem),如果除数为零,则会抛出算术异常。
Java 虚拟机对浮点数的操作按照 IEEE 754 规定执行。特别是,Java 虚拟机需要完全支持 IEEE 754 非规范浮点数和渐进下溢,这使得更容易证明特定数值算法的期望性质。
Java 虚拟机要求浮点运算的行为如同每个浮点运算符都将其浮点结果四舍五入到结果精度一样。
不精确的结果必须四舍五入到最接近无限精确结果的可表示值;如果两个最接近的可表示值彼此相等接近,则选择其最低有效位为零的那个值。这是 IEEE 754 标准的默认舍入模式,被称为“舍入到最近值”模式。
Java 虚拟机在将浮点值转换为整数时使用 IEEE 754 向零舍入模式。这会导致数字被截断;表示操作数值小数部分的任何有效位的位都被丢弃。向零舍入模式选择其结果为与无限精确结果最接近但不比其大绝对值的该类型值。
Java 虚拟机的浮点运算符不会抛出运行时异常(不要与 IEEE 754 浮点异常混淆)。溢出操作会产生有符号无穷大值,下溢操作会产生非规范化值或有符号零值,而没有数学确定结果的操作会产生 NaN。所有以 NaN 作为操作数的数值运算都会产生 NaN 作为结果。
对于 long 类型值的比较(lcmp)执行有符号比较。 对于浮点类型值的比较(dcmpg、dcmpl、fcmpg、fcmpl)是 使用 IEEE 754 无符号比较的方式执行。
类型转换指令
类型转换指令允许在 Java 虚拟机的数值类型之间进行转换。这些指令可用于在用户代码中实现显式转换,或者缓解 Java 虚拟机指令集缺乏正交性的问题。
Java 虚拟机直接支持以下的数值扩大转换:
- int 类型转换为 long 类型、float 类型或 double 类型
- long 类型转换为 float 类型或 double 类型
- float 类型转换为 double 类型
数值扩大转换指令为 i2l、i2f、i2d、l2f、l2d 和 f2d。这些操作码的助记符根据类型指令的命名约定以及使用 2 表示“到”的约定是直观易懂的。例如,i2d 指令将 int 值转换为 double 类型。
大多数数值扩大转换不会丢失数值整体大小的信息。实际上,从 int 类型转换为 long 类型和从 int 类型转换为 double 类型都不会丢失任何信息;数值值会被精确保留。从 float 类型转换为 double 类型且为 FP-严格的转换也会精确保留数值值;只有那些不是 FP-严格的转换才可能丢失转换值整体大小的信息。从整型(int)转换为浮点型(float),或者从长整型(long)转换为浮点型(float),或者从长整型(long)转换为双精度型(double), 可能会丢失精度,即可能会丢失数值中一些最不显著的位; 转换后的浮点型值是整数值经过正确舍入后的版本, 采用 IEEE 754 向最近值舍入的模式。
尽管可能会出现精度损失的情况,但数值类型的扩大转换 永远不会导致 Java 虚拟机抛出运行时异常(不要与 IEEE 754 浮点异常相混淆)。 将整型(int)扩大转换为长整型(long)只是将整型值的补码表示符号扩展到更宽的格式以填充该格式。将字符型(char)扩大转换为整型类型(zero-extends 表示将字符值的表示扩展到更宽的格式以填充该格式)。 请注意,从整型(byte、char 和 short)到整型(int)的数值类型扩大转换不存在。正如 §2.11.1 中所指出的,这些类型的值在内部会扩展为整型(int),从而使这些转换隐式进行。
Java 虚拟机还直接支持以下几种数值类型的缩小转换: - int 类型转换为 byte、short 或 char 类型
- long 类型转换为 int 类型
- float 类型转换为 int 或 long 类型
- double 类型转换为 int、long 或 float 类型 数值类型的缩小转换指令为 i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。数值类型的缩小转换可能会导致结果值具有不同的符号、不同的数量级,或者两者兼而有之;这样一来,可能会丢失精度。 将 int 或 long 类型的数值转换为整型类型 T(T 可以是 int 或 long)时,会简单地丢弃除最低位 n 个二进制位之外的所有二进制位,其中 n 是表示类型 T 所需的二进制位数。这可能导致所得值与输入值具有不同的符号。 在将浮点值缩小转换为整型类型 T(T 可以是 int 或 long)时,如果 T 是 int 或 long,浮点值的转换方式如下:
- 如果浮点值为 NaN,转换的结果为 int 或 long 类型的 0。
- 否则,如果浮点值不是无穷大,将使用 IEEE 754 向零舍入模式将浮点值转换为整数值 V。有两种情况:- 如果 T 是长整型,并且这个整数值能够表示为长整型,则结果就是整型值 V。
- 如果 T 是整型,并且这个整数值能够表示为整型,则结果就是整型值 V。
- 否则:
- 该值要么太小(一个大数值的负值或负无穷大),结果就是整型或长整型所能表示的最小值;
- 或者该值要么太大(一个大数值的正值或正无穷大),结果就是整型或长整型所能表示的最大值。
从双精度浮点数到单精度浮点数的窄化数值转换遵循 IEEE 754 的规定。结果使用 IEEE 754 向近似模式进行正确舍入。无法表示为单精度浮点数的值会被转换为浮点型的正零或负零;无法表示为单精度浮点数的值会被转换为正无穷大或负无穷大。双精度 NaN 总是会被转换为浮点 NaN。
尽管可能会发生溢出、下溢或精度损失等情况,但数值类型之间的缩小转换永远不会导致 Java 虚拟机抛出运行时异常(不要与 IEEE 754 浮点异常相混淆)。
对象的创建与操作
虽然类实例和数组都是对象,但Java虚拟机使用不同的指令集来创建和操作类实例和数组:
- 创建新的类实例:new。
- 创建新数组:newarray, anewarray, multianewarray。
- 访问类的字段(静态字段,称为类变量)和类实例的字段(非静态字段,称为实例变量):getstatic, putstatic, getfield, putfield。
- 将数组组件加载到操作数栈上:baload, caload, saload, iaload, laload, faload, daload, aaload。
- 从操作数栈存储值作为数组组件:bastore, castore, sastore, iastore, lastore, fastore, dastore, aastore。
- 获取数组长度:arraylength。
- 检查类实例或数组的属性:instanceof, checkcast。
操作数栈管理指令
提供了一些指令用于直接操作操作数栈: pop, pop2, dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2, swap.
控制传输指令
控制转移指令会有条件地或无条件地导致 Java 虚拟机继续执行除控制转移指令之后的指令之外的其他指令。它们是:
- 条件分支:ifeq、ifne、iflt、ifle、ifgt、ifle、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 指定具体的值。 使用 int 比较指令执行类型为 boolean、byte、char 和 short 的数据之间的比较条件分支。使用比较数据并产生比较结果的 int 类型的指令来启动类型为 long、float 或 double 的数据之间的比较条件分支。后续的 int 比较指令测试此结果并实现条件分支。由于其对 int 比较的强调,Java 虚拟机为 int 类型提供了丰富的条件分支指令集。 所有 int 类型的条件控制转移指令执行有符号比较。
方法调用与返回指令
以下五条指令调用方法:
- invokevirtual 调用对象的实例方法,根据对象的(虚拟)类型进行分派。这是 Java 编程语言中的正常方法分派。
- invokeinterface 调用接口方法,搜索特定运行时对象实现的方法以找到适当的方法。
- invokespecial 调用需要特殊处理的实例方法,无论是实例初始化方法、私有方法还是超类方法。
- invokestatic 调用命名类中的类(静态)方法。
- invokedynamic指令调用与invokedynamic指令绑定的调用站点对象的目标方法。 该调用站点对象由Java虚拟机通过在指令首次执行之前运行引导方法,绑定到invokedynamic指令的特定词法实例。 因此,每个invokedynamic指令实例具有唯一的链接状态,这与其他调用方法的指令不同。
返回指令根据返回类型有所不同,包括ireturn(用于返回boolean、byte、char、short或int类型的值)、lreturn、freturn、dreturn和areturn。 此外,return指令用于从声明为void的方法、实例初始化方法以及类或接口初始化方法中返回。
抛出异常
异常是通过athrow指令以编程方式抛出的。如果检测到异常情况,各种Java虚拟机指令也会抛出异常。
同步
Java 虚拟机支持对方法内部以及方法内部指令序列的同步操作,这通过单一的同步构造体(即监视器)来实现。
方法级别的同步是隐式地执行的,作为方法调用和返回的一部分。一个同步方法在运行时常量池的方法_info 结构中通过 ACC_SYNCHRONIZED 标志进行区分,该标志由方法调用指令进行检查。当调用设置了 ACC_SYNCHRONIZED 的方法时,执行线程会进入监视器,调用该方法本身,并在方法调用正常完成或突然中断的情况下退出监视器。在执行线程拥有监视器期间,其他线程无法进入该监视器。如果在调用同步方法期间抛出异常且同步方法未处理该异常,则在异常被从同步方法重新抛出之前,该方法的监视器会自动退出。
指令序列的同步通常用于对 Java 编程语言中的同步块进行编码。Java 虚拟机向支持此类语言的 monitorenter 和 monitorexit 指令提供同步块的同步操作说明。
同步块的正确实现需要针对 Java 虚拟机进行编译器的合作。
结构化锁的情况是在方法调用期间,给定监视器上的每个退出操作都与该监视器上的先前进入操作相匹配。由于无法保证提交给 Java 虚拟机的所有代码都会执行结构化锁操作,因此 Java 虚拟机的实现者可以但无需强制执行以下两个保证结构化锁的规则。设 T 为一个线程,M 为一个监视器。那么:
- 在方法调用期间,T 对 M 执行的监视器进入操作的数量必须等于 T 在该方法调用期间对 M 执行的监视器退出操作的数量,无论该方法调用是正常完成还是突然中断。
- 在方法调用期间,T 对 M 执行的监视器退出操作次数绝不能超过 M 对 T 执行的监视器进入操作次数,而这个次数限制是在方法调用期间才生效的。 请注意,当 Java 虚拟机调用同步方法时自动执行的监视器进入和退出操作被视为发生在调用方法的调用期间。
类库
Java 虚拟机必须为 Java SE 平台的类库的实现提供足够的支持。这些类库中的某些类若没有 Java 虚拟机的协作则无法实现。 可能需要 Java 虚拟机提供特殊支持的类包括:
- 反射,例如 java.lang.reflect 包中的类以及 Class 类。
- 类或接口的加载和创建。最明显的例子是 ClassLoader 类。
- 类或接口的链接和初始化。上述引用的示例类也属于这一类。 -0 安全性,例如 java.security 包中的类以及其他类,如 SecurityManager 类。
- 多线程,例如 Thread 类。
- 弱引用,例如 java.lang.ref 包中的类。 上述列表旨在作为示例而非详尽无遗。这些类或它们所提供的功能的详尽列表超出了本规范的范围。有关这些类或它们所提供的功能的详细信息,请参阅 Java SE 平台类库的规范。
公共设计,私有实现
到目前为止,本规范概述了Java虚拟机的公共视图:类文件格式和指令集。这些组件对于Java虚拟机的硬件、操作系统和实现无关性至关重要。公共设计,私有实现 JAVA 虚拟机的结构。实施者可能更愿意将它们视为在每个实现Java SE平台的主机之间安全传递程序片段的手段,而不是作为必须完全遵循的蓝图。
了解公共设计与私有实现之间的界限在哪里是很重要的。Java虚拟机的实现必须能够读取类文件,并且必须准确地实现其中的Java虚拟机代码的语义。一种实现方法是将本文档作为规范,并逐字实现该规范。然而,在本规范的约束范围内,实现者修改或优化其实现也是完全可行和可取的。只要能够读取类文件格式并保持其代码的语义,实现者可以以任何方式实现这些语义。“引擎盖下”的内容是实现者的事情,只要正确地维护外部接口即可。
有一些例外情况:调试器、分析器和即时代码生成器可能都需要访问通常被认为是Java虚拟机“内部实现”的元素。在适当的情况下,Oracle会与其他Java虚拟机实现者和工具供应商合作,开发用于此类工具的Java虚拟机通用接口,并在行业内推广这些接口。
实现者可以利用这种灵活性来定制高性能、低内存使用或可移植性的Java虚拟机实现。给定实现中合理的选择取决于该实现的目标。实现选项的范围包括以下内容:
-
在加载时或执行期间将Java虚拟机代码翻译成另一台虚拟机的指令集。
-
在加载时或执行期间将Java虚拟机代码翻译成主机CPU的本地指令集(有时称为即时编译,或JIT代码生成)。
精确定义的虚拟机和对象文件格式的存在不必显著限制实现者的创造力。Java虚拟机被设计为支持许多不同的实现,提供新的和有趣的解决方案,同时保持实现之间的兼容性。