由Java虚拟机执行的编译代码使用与硬件和操作系统无关的二进制格式表示,通常存储在 class文件 中。class文件 精确地定义了类或接口的表示形式,包括在特定于平台的目标文件格式中可能被视为理所当然的字节排序等细节。
数据类型
与Java 编程语言一样,Java 虚拟机对两种类型进行操作:基本类型 和 引用类型。相应地,有两种类型的值可以存储在变量中,作为参数传递,由方法返回,并对其进行操作:基本值 和 引用值。
Java 虚拟机期望几乎所有类型检查都在运行时之前完成,通常由编译器完成,而不必由 Java 虚拟机本身完成。基本类型的值不需要被标记,也不需要在运行时检查以确定它们的类型,或者与引用类型的值区分开来。相反,Java 虚拟机的指令集使用对特定类型的值进行操作的指令来区分其操作数类型。例如,iadd、ladd、fadd 和 dadd 都是添加两个数值并产生数值结果的 Java 虚拟机指令,但每个指令都专门针对其操作数类型: int、long、float 和 double。
Java 虚拟机包含对对象的显式支持。对象要么是动态分配的类实例,要么是数组。对对象的引用被认为具有 Java 虚拟机类型引用。可以将引用类型的值视为指向对象的指针。一个对象可能存在多个引用。对象总是通过引用类型的值进行操作、传递和测试。
基本类型和值
Java虚拟机支持的基本数据类型有 数值类型、布尔类型 和returnAddress类型。
数值类型由 整数类型 和 浮点类型 组成:
整数类型包括:byte、short、short、int、long 和 char。
浮点类型包括:float 和 double。
布尔类型 的值将 true 和 false 编码,默认值为false。
returnAddress类型 的值是指向 Java 虚拟机指令操作码的指针。在基本类型中,只有 returnAddress类型与 Java 编程语言类型没有直接关联。
引用类型和值
有三种引用类型:类类型、数组类型 和 接口类型。它们的值分别是对动态创建的类实例、数组或实现接口的类实例或数组的引用。
数组类型 由具有单个维度的组件类型组成(其长度不由类型给出)。数组类型 的组件类型本身可以是 数组类型。如果从任何 数组类型 开始,先考虑其组件类型,然后再考虑(如果也是数组类型)该类型的组件类型,依此类推,则最终必须达到不是 数组类型 的组件类型,这称为数组类型的元素类型。数组类型的元素类型必须是原始类型,类类型或接口类型。
引用值 也可以是特殊的空引用,一个对 no 对象的引用,在这里用 null 表示。空引用最初没有运行时类型,但可以转换为任何类型。引用类型的默认值是 null。
运行时数据区
Java 虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区域是在Java 虚拟机启动时创建的,只有在Java虚拟机退出时才会销毁。其他数据区域是在每个线
程创建时创建,在线程退出时销毁。
程序计数器(Program Counter Register)
Java 虚拟机可以同时支持多个线程执行。每个 Java 虚拟机线程都有自己的 程序计数器 。在任何时候,每个 Java 虚拟机线程都在执行单个方法的代码,即该线程的当前方法。如果该方法不是 native,程序计数器 包含当前正在执行的 Java 虚拟机指令的地址。如果线程当前执行的方法是 native,则 Java 虚拟机的 程序计数器 的值是未定义的。Java 虚拟机的 程序计数器 足够宽,可以容纳特定平台上的 returnAddress 或本机指针。
Java 虚拟机栈
每个 Java 虚拟机线程都有一个私有的 Java 虚拟机栈,与线程同时创建。Java 虚拟机栈 存储 栈帧。Java 虚拟机栈类似于 C 等传统语言的栈:它持有局部变量和部分结果,并在方法调用和返回中起作用。因为除了 push 和 pop 栈帧 外,Java 虚拟机栈 从来没有被直接操作过,所以可以对栈帧进行堆分配。Java虚拟机栈的内存不需要是连续的。
Java 虚拟机栈具有固定的大小,或者根据计算的需要动态扩展和收缩。如果 Java 虚拟机栈的大小是固定的,则可以在创建栈时独立选择每个 Java 虚拟机栈的大小。
Java 虚拟机实现可以为程序员或用户提供对 Java 虚拟机栈初始大小的控制,在动态扩展或收缩 Java 虚拟机栈的情况下,还可以提供对最大和最小大小的控制。
下列异常情况与Java 虚拟机栈相关:
如果线程中的计算需要比允许的更大的 Java 虚拟机栈,则 Java 虚拟机将抛出一个 StackOverflowError 异常。
如果可以动态地扩展的 Java 虚拟机栈,扩展时可用内存不足,或者内存不足以创建一个新线程的初始 Java虚拟机栈,Java 虚拟机抛出一个 OutOfMemoryError 异常。
本地方法栈
Java 虚拟机的实现可以使用传统栈(俗称“C栈”)来支持 native 方法(用 Java 编程语言以外的语言编写的方法)。 和 Java 虚拟机栈 一样,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
堆
Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的 堆。堆是为所有类实例和数组分配内存的运行时数据区域。
堆 是在虚拟机启动时创建的。对象的 堆 存储由自动存储管理系统(称为 垃圾收集器)回收;对象从不显式释放。Java 虚拟机没有特定类型的自动存储管理系统,可以根据实现者的系统需求选择存储管理技术。堆 的大小可以是固定的,也可以根据计算的需要进行扩展,如果不需要更大的堆,则可以收缩。堆的内存不需要是连续的。
Java 虚拟机实现可以为程序员或用户提供对 堆 初始大小的控制,如果可以动态扩展或收缩堆,还需要可以控制 堆 的最大和最小大小。
下列异常情况与 堆 相关:
如果计算需要的 堆 比自动存储管理系统提供的 堆 多,则 Java 虚拟机抛出OutOfMemoryError。
方法区
Java 虚拟机具有一个在所有 Java 虚拟机线程之间共享的 方法区。方法区 类似于常规语言的编译代码的存储区域,或者类似于操作系统过程中的“文本”段。它存储每个类的结构,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码,包括用于类和接口初始化以及实例初始化的特殊方法。
方法区 是在虚拟机启动时创建的。尽管 方法区 在逻辑上是堆的一部分,但是简单的实现可以选择不进行垃圾回收或压缩。没有规定· 方法区 的位置或用于管理已编译代码的策略。方法区可以是固定大小的,或者可以根据计算的需要进行扩展,如果不需要更大的方法区域,则可以缩小。方法区的内存不必是连续的。
Java 虚拟机实现可以为程序员或用户提供对方法区 初始大小的控制,并且在方法区域大小可变的情况下,可以控制最大和最小 方法区 大小。
以下异常条件与 方法区 相关联:
如果无法提供 方法区 中的内存来满足分配请求,则 Java 虚拟机将抛出一个 OutOfMemoryError。
运行时常量池
运行时常量池 是 class 文件 中 constant_pool 表的每个类或每个接口的运行时表示。它包含几种类型的常量,从编译时已知的数值常量到必须在运行时解析的方法和字段引用。运行时常量池 的功能类似于传统编程语言的符号表,尽管它包含的数据范围比典型的符号表更广。
每个 运行时常量池 都是从 Java 虚拟机的 方法区 分配的。类或接口的 运行时常量池 是在Java虚拟机创建类或接口时构造的。
下列异常情况与类或接口的运行时常量池的构造有关:
在创建类或接口时,如果构建运行时常量池所需的内存超过了 Java 虚拟机的 方法区 所能提供的内存,Java 虚拟机抛出 OutOfMemoryError。
栈帧
栈帧 用于存储数据和部分结果,以及执行动态链接、方法的返回值和调度异常。
每次调用一个方法都会创建一个新 栈帧。无论完成是正常的还是异常的(它抛出一个未捕获的异常),当它的方法调用完成时,一个 栈帧 将被销毁。栈帧 是从创建 栈帧 的线程的 Java 虚拟机栈中分配的。每一帧都有自己的 局部变量 数组,自己的 操作数栈,以及对当前方法类的 运行时常量池 的引用。
可以使用附加的特定于实现的信息(如调试信息)对 栈帧 进行扩展。
局部变量数组 和 操作数栈 的大小是在编译时确定的,并随与 栈帧 关联的方法的代码一起提供。因此,栈帧 数据结构的大小只取决于 Java 虚拟机的实现,并且可以在方法调用时同时分配这些结构的内存。
在给定的控制线程中,只有一个帧(执行方法的帧)是活动的。这个帧称为当前帧,它的方法称为当前方法。定义当前方法的类是当前类。对 局部变量 和 操作数栈 的操作通常与当前帧相关。
如果一个帧的方法调用另一个方法,或者该帧的方法完成,则该帧将停止为当前帧。当调用一个方法时,将创建一个新 栈帧,并在控制转移到新方法时成为当前 栈帧。在方法返回时,当前帧将其方法调用的结果(如果有的话)传回前一帧。当前一帧成为当前帧时,当前帧将被丢弃。
注意,线程创建的 栈帧 是该线程的本地 栈帧,不能被任何其他线程引用。
局部变量
每一 栈帧 包含一组称为 局部变量 的变量。栈帧 的 局部变量 数组的长度在编译时确定,并以类或接口的二进制表示形式提供,同时提供与帧相关的方法的代码。
单个 局部变量 可以保存类型为 boolean、byte、char、short、int、float、reference或returnAddress的值。一对 局部变量 可以包含 long 或 double 类型的值。
局部变量 通过索引寻址。第一个 局部变量 的索引为 0。当且仅当该整数比局部变量数组的大小小 0 到 1 之间时,该整数被认为是 局部变量 数组的索引。
long 或 double 类型的值占用两个连续的局部变量。这样的值只能使用较小的索引来处理。例如,在索引 n 处存储在局部变量数组中的 double 类型的值实际上占用了索引为 n 和 n+1 的局部变量;但是,无法从索引 n+1 处加载局部变量。它可以存储。但是,这样做会使局部变量 n 的内容无效。
Java 虚拟机不需要 n 是偶数。直观地说,long 和 double 类型的值 在局部变量 数组中不必是 64 位对齐的。实现者可以自由决定使用为该值保留的两个 局部变量 来表示这些值的适当方法。
Java 虚拟机使用本地变量在方法调用时传递参数。在类方法调用中,任何参数都是在连续的 局部变量 中传递的,从 局部变量 0 开始。在实例方法调用中,总是使用局部变量 0 将引用传递给调用实例方法的对象(在Java编程语言中)。任何参数随后都在从局部变量 1 开始的连续局部变量中传递。
操作数栈
每个 栈帧 都包含一个后进先出(LIFO)堆栈,称为操作数栈。帧的操作数栈的最大深度是在编译时确定的,并与帧关联的方法的代码一起提供。
当上下文清楚时,我们有时会将当前帧的操作数栈简单地称为操作数栈。
创建包含操作数栈的 栈帧 时,该 操作数栈 为空。Java 虚拟机提供将常量或值从局部变量或字段加载到操作数栈的指令。其他 Java 虚拟机指令从操作数栈中获取操作数,对它们进行操作,并将结果推回操作数栈。操作数栈还用于准备传递给方法的参数和接收方法结果。
例如,iadd 指令将两个 int 值相加。它要求将 int 值添加到 操作数栈 的前两个值中,这是由前面的指令推入的。两个 int 值都是从 操作数栈 中弹出的。它们被添加,它们的和被推回 操作数栈。子计算可以嵌套在 操作数栈 上,从而产生可由包含计算使用的值。
操作数栈 上的每个条目可以包含任何 Java 虚拟机类型的值,包括 long 或 double 类型的值。
操作数堆栈中的值必须以适合其类型的方式操作。例如,不可能推两个 int 值,然后将它们视为 long 值;也不可能推两个浮点值,然后用 iadd 指令将它们相加。少量的 Java 虚拟机指令(dup 指令和swap)作为原始值对运行时数据区域进行操作,而不考虑它们的具体类型;这些指令的定义方式使它们不能用于修改或分解单个值。这些操作数栈操作的限制是通过类文件验证来实施的。
在任何时候,操作数堆栈都有一个关联的深度,其中 long 或 double 类型的值为深度贡献两个单位,其他类型的值为深度贡献一个单位。
动态链接
每一 栈帧 都包含对当前方法类型的 运行时常量池 的引用,以支持方法代码的 动态链接。方法的类文件代码引用要调用的方法和要通过符号引用访问的变量。动态链接 将这些符号方法引用转换为具体的方法引用,根据需要加载类来解析尚未定义的符号,并将变量访问转换为与这些变量的运行时位置相关的存储结构中的适当偏移量。
方法和变量的这种后期绑定使方法使用的其他类中的更改不太可能破坏此代码。
方法正常调用完成
如果方法调用没有导致抛出异常,则方法调用正常完成,无论是直接从 Java 虚拟机抛出,还是执行显式抛出语句的结果。如果当前方法的调用正常完成,则可能向调用方法返回一个值。当被调用的方法执行一条返回指令时,就会发生这种情况,对于返回的值的类型(如果有的话),必须选择一条返回指令。
在这种情况下,使用当前 栈帧 来恢复调用程序的状态,包括它的局部变量 和 操作数栈,并适当增加调用程序的 程序计数器 以跳过方法调用指令。然后在调用方法的框架中继续正常执行,将返回的值(如果有的话)推入该 栈帧 的 操作数栈。
方法异常调用完成
如果在方法中执行 Java 虚拟机指令导致 Java 虚拟机抛出异常,并且该异常不在方法中处理,则方法调用将突然结束。athrow 指令的执行也会导致显式抛出异常,如果当前方法没有捕获异常,则会导致方法调用的异常完成。异常完成的方法调用永远不会向调用者返回值。
对象的表示
Java 虚拟机不强制要求对象具有任何特定的内部结构。
Oracle 对 Java 虚拟机的实现,对类实例的引用句柄指针,指针本身就是一对:一个包含对象方法表的指针和一个指向类对象的指针,表示对象的类型,以及其他为对象从堆中分配的内存数。