Java虚拟机规范
来自GPT翻译,主要是找一些跟jvm指令与字节码相关的部分,来源:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
2.5. Run-Time Data Areas
Java虚拟机定义了各种运行时数据区,这些数据区在程序执行期间使用。其中一些数据区在Java虚拟机启动时创建,并且只有在Java虚拟机退出时才被销毁。其他数据区是每个线程的。每个线程的数据区在线程创建时创建,在线程退出时销毁。
2.5.1. The pc Register
Java虚拟机可以同时支持多个执行线程(JLS §17)。每个Java虚拟机线程都有自己的pc(程序计数器)寄存器。在任何时刻,每个Java虚拟机线程都在执行单个方法的代码,即该线程的当前方法(§2.6)。如果该方法不是本地方法,则pc寄存器包含当前正在执行的Java虚拟机指令的地址。如果线程当前执行的方法是本地方法,则Java虚拟机的pc寄存器的值是未定义的。Java虚拟机的pc寄存器足够宽,可以在特定平台上保存returnAddress或本地指针。
2.5.2. Java Virtual Machine Stacks
每个Java虚拟机线程都有一个私有的Java虚拟机堆栈,与线程同时创建。Java虚拟机堆栈存储帧(§2.6)。Java虚拟机堆栈类似于传统语言(如C)的堆栈:它保存本地变量和部分结果,并在方法调用和返回中起作用。由于Java虚拟机堆栈除了推送和弹出帧之外永远不会直接操作,因此帧可以在堆上分配。Java虚拟机堆栈的内存不需要是连续的。
在第一版《Java®虚拟机规范》中,Java虚拟机堆栈被称为Java堆栈。
本规范允许Java虚拟机堆栈的大小为固定大小或根据计算需要动态扩展和收缩。如果Java虚拟机堆栈的大小是固定的,则可以在创建该堆栈时独立选择每个Java虚拟机堆栈的大小。
Java虚拟机实现可以为程序员或用户提供对Java虚拟机堆栈的初始大小的控制,以及在动态扩展或收缩Java虚拟机堆栈的情况下,对最大和最小大小的控制。
以下异常情况与Java虚拟机堆栈相关:
如果线程中的计算需要比允许的Java虚拟机堆栈更大的Java虚拟机堆栈,则Java虚拟机会抛出StackOverflowError。
如果Java虚拟机堆栈可以动态扩展,并且尝试扩展但无法提供足够的内存以实现扩展,或者无法提供足够的内存以创建新线程的初始Java虚拟机堆栈,则Java虚拟机会抛出OutOfMemoryError。
2.5.3. Heap
Java虚拟机具有一个堆,该堆在所有Java虚拟机线程之间共享。堆是运行时数据区,用于分配所有类实例和数组的内存。
堆在虚拟机启动时创建。对象的堆存储由自动存储管理系统(称为垃圾收集器)回收;对象从未明确释放。Java虚拟机不假定任何特定类型的自动存储管理系统,并且存储管理技术可以根据实现者的系统要求进行选择。堆可以是固定大小的,也可以根据计算需要进行扩展,并且如果不需要更大的堆,则可以缩小堆。堆的内存不需要是连续的。
Java虚拟机实现可以为程序员或用户提供对堆的初始大小的控制,以及如果堆可以动态扩展或缩小,则对最大和最小堆大小的控制。
以下异常情况与堆相关:
如果计算需要比自动存储管理系统可用的堆更多的堆,则Java虚拟机会抛出OutOfMemoryError。
2.5.4. Method Area
Java虚拟机具有一个方法区,该方法区在所有Java虚拟机线程之间共享。方法区类似于传统语言的编译代码存储区或类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据以及方法和构造函数的代码,包括用于类和实例初始化以及接口初始化的特殊方法(§2.9)。
方法区在虚拟机启动时创建。虽然方法区在逻辑上是堆的一部分,但简单的实现可能选择不进行垃圾收集或压缩。本规范不强制执行方法区的位置或用于管理编译代码的策略。方法区可以是固定大小的,也可以根据计算需要进行扩展,并且如果不需要更大的方法区,则可以缩小方法区。方法区的内存不需要是连续的。
Java虚拟机实现可以为程序员或用户提供对方法区初始大小的控制,在方法区大小可变的情况下,还可以控制最大和最小方法区大小。
以下异常情况与方法区相关:
如果方法区中的内存无法满足分配请求,则Java虚拟机会抛出OutOfMemoryError。
2.5.5. Run-Time Constant Pool
运行时常量池是类文件(§4.4)中常量池表的每个类或每个接口的运行时表示。它包含多种常量,从编译时已知的数字文字到必须在运行时解析的方法和字段引用。运行时常量池的功能类似于传统编程语言的符号表,尽管它包含比典型符号表更广泛的数据。
每个运行时常量池都分配自Java虚拟机的方法区(§2.5.4)。当Java虚拟机创建类或接口(§5.3)时,将构建该类或接口的运行时常量池。
以下异常条件与类或接口的运行时常量池的构建相关:
当创建类或接口时,如果运行时常量池的构建需要比Java虚拟机的方法区可用的内存更多,则Java虚拟机会抛出OutOfMemoryError。
有关运行时常量池的构建信息,请参见§5(加载,链接和初始化)。
2.5.6. Native Method Stacks
Java虚拟机的实现可以使用传统堆栈(俗称“C堆栈”)来支持本地方法(使用Java编程语言以外的语言编写的方法)。本地方法堆栈也可以被用于Java虚拟机指令集的解释器实现,例如使用C语言。不能加载本地方法且不依赖于传统堆栈的Java虚拟机实现不需要提供本地方法堆栈。如果提供了本地方法堆栈,则通常在创建每个线程时分配本地方法堆栈。
本规范允许本地方法堆栈的大小为固定大小或根据计算需要动态扩展和收缩。如果本地方法堆栈的大小为固定大小,则可以在创建该堆栈时独立选择每个本地方法堆栈的大小。
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. Local Variables
每个帧(§2.6)都包含一个变量数组,称为其本地变量。帧的本地变量数组的长度在编译时确定,并在类或接口的二进制表示中与与帧相关联的方法的代码(§4.7.3)一起提供。
单个本地变量可以保存类型为布尔型、字节型、字符型、短整型、整型、浮点型、引用类型或返回地址类型的值。一对本地变量可以保存类型为长整型或双精度浮点型的值。
本地变量通过索引进行寻址。第一个本地变量的索引为零。只有当整数在零到本地变量数组大小减一之间时,该整数才被视为本地变量数组的索引。
类型为长整型或双精度浮点型的值占用两个连续的本地变量。这样的值只能使用较小的索引进行寻址。例如,存储在索引n处的双精度浮点型值实际上占用索引n和n+1的本地变量;但是,索引n+1的本地变量不能被加载。它可以被存储。但是,这样做会使本地变量n的内容无效。
Java虚拟机不要求n是偶数。直观地说,类型为长整型和双精度浮点型的值不需要在本地变量数组中对齐为64位。实现者可以自由决定使用为该值保留的两个本地变量来表示这样的值的适当方式。
Java虚拟机使用本地变量在方法调用时传递参数。在类方法调用中,任何参数都在连续的本地变量中传递,从本地变量0开始。在实例方法调用中,本地变量0始终用于传递对正在调用实例方法的对象的引用(在Java编程语言中为this)。随后,任何参数都在连续的本地变量中传递,从本地变量1开始。
2.6.2. 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的值对深度贡献两个单位,任何其他类型的值对深度贡献一个单位。
2.6.3. Dynamic Linking
每个帧(§2.6)都包含对当前方法类型的运行时常量池(§2.5.5)的引用,以支持方法代码的动态链接。方法的类文件代码通过符号引用引用要调用的方法和要访问的变量。动态链接将这些符号方法引用转换为具体方法引用,必要时加载类以解析尚未定义的符号,并将变量访问转换为与这些变量的运行时位置相关联的存储结构中的适当偏移量。
这种方法和变量的后期绑定使得在方法使用的其他类中进行更改不太可能破坏此代码。
2.6.4. Normal Method Invocation Completion
如果方法调用不会导致异常(§2.10)被抛出,无论是直接从Java虚拟机还是作为执行显式throw语句的结果,该方法调用将正常完成。如果当前方法的调用正常完成,则可以向调用方法返回一个值。当被调用的方法执行其中一个return指令(§2.11.8)时,必须选择适合返回值类型(如果有)的指令。
在这种情况下,当前帧(§2.6)用于恢复调用者的状态,包括其本地变量和操作数栈,调用者的程序计数器适当地增加以跳过方法调用指令。然后,在调用方法的帧中正常继续执行,返回值(如果有)被推送到该帧的操作数栈上。
2.6.5. Abrupt Method Invocation Completion
如果方法内部执行Java虚拟机指令导致Java虚拟机抛出异常(§2.10),并且该异常在方法内部未被处理,则方法调用将突然完成。执行athrow指令(§athrow)也会导致显式抛出异常,如果当前方法未捕获异常,则会导致方法调用突然完成。突然完成的方法调用永远不会向其调用者返回值。
2.9. Special Methods
在Java虚拟机层面上,Java编程语言中的每个构造函数(JLS §8.8)都会出现为一个具有特殊名称
一个类或接口最多有一个类或接口初始化方法,并通过调用该方法进行初始化(§5.5)。类或接口的初始化方法具有特殊名称
类文件中命名为
在版本号为51.0或更高的类文件中,该方法必须另外设置其ACC_STATIC标志(§4.6)才能成为类或接口初始化方法。
这个要求是在Java SE 7中引入的。在版本号为50.0或以下的类文件中,一个名为
名称
如果以下所有条件都为真,则方法是签名多态的:
它在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包。
2.10. Exceptions
Java虚拟机中的异常由Throwable类或其子类的实例表示。抛出异常会导致控制从抛出异常的点立即非本地转移。
大多数异常都是同步发生的,是由它们发生的线程的操作导致的。相比之下,异步异常可能会在程序的执行过程中的任何时刻发生。Java虚拟机出于以下三个原因之一抛出异常:
执行了athrow指令(§athrow)。
Java虚拟机同步检测到异常执行条件。这些异常不会在程序的任意点抛出,而是在执行指令后同步抛出,该指令指定异常为可能的结果,例如:
当指令包含违反Java编程语言语义的操作时,例如在数组边界之外进行索引。
当程序的某个部分加载或链接时发生错误。
当某个资源的某个限制被超出时,例如使用了过多的内存。
异步异常发生的原因是:
调用了Thread或ThreadGroup类的stop方法,或
Java虚拟机实现中发生了内部错误。
stop方法可以由一个线程调用以影响另一个线程或指定线程组中的所有线程。它们是异步的,因为它们可能在其他线程的执行过程中的任何时刻发生。内部错误被认为是异步的(§6.3)。
Java虚拟机可能允许在异步异常抛出之前发生少量但有限的执行。这种延迟是为了允许优化代码在遵守Java编程语言语义的前提下,在实际处理这些异常的点上检测并抛出它们。
一个简单的实现可能会在每个控制转移指令的点上轮询异步异常。由于程序具有有限的大小,这提供了检测异步异常的总延迟的上限。由于在控制转移之间不会发生任何异步异常,代码生成器可以在控制转移之间重新排序计算以获得更好的性能。建议进一步阅读Marc Feeley的论文《在股票硬件上有效轮询》,发表于1993年的函数式编程和计算机架构会议论文集,丹麦哥本哈根,第179-187页。
Java虚拟机抛出的异常是精确的:当控制转移发生时,从抛出异常的点之前执行的指令的所有效果必须看起来已经发生了。从抛出异常的点之后执行的任何指令都不能看起来已经被评估。如果优化代码已经猜测执行了一些在异常发生点之后的指令,那么这样的代码必须准备好将这种猜测执行从程序的用户可见状态中隐藏起来。
Java虚拟机中的每个方法都可以与零个或多个异常处理程序相关联。异常处理程序指定了实现该方法的Java虚拟机代码的偏移量范围,描述了异常处理程序能够处理的异常类型,并指定了处理该异常的代码的位置。如果异常的指令偏移量在异常处理程序的偏移量范围内,并且异常类型是与异常处理程序处理的异常类相同或是其子类,那么异常就与异常处理程序匹配。当抛出异常时,Java虚拟机会在当前方法中搜索匹配的异常处理程序。如果找到匹配的异常处理程序,则系统会跳转到匹配处理程序指定的异常处理代码。
如果在当前方法中找不到相应的异常处理程序,当前方法调用将会突然完成(§2.6.5)。在突然完成时,当前方法调用的操作数栈和局部变量将被丢弃,其帧将被弹出,恢复调用方法的帧。然后,在调用者的帧上下文中重新抛出异常,以此类推,继续向上执行方法调用链。如果在到达方法调用链的顶部之前找不到合适的异常处理程序,则抛出异常的线程的执行将被终止。
方法的异常处理程序的搜索顺序很重要。在类文件中,每个方法的异常处理程序都存储在一个表格中(§4.7.3)。在运行时,当抛出异常时,Java虚拟机按照它们在相应的异常处理程序表格中出现的顺序,从该表格的开头开始,搜索当前方法的异常处理程序。
请注意,Java虚拟机不强制执行方法的异常表条目的嵌套或任何排序。Java编程语言的异常处理语义仅通过与编译器的合作实现(§3.12)。当通过其他方式生成类文件时,定义的搜索过程确保所有Java虚拟机实现都会表现出一致的行为。
2.11. Instruction Set Summary
一个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虚拟机操作码为一个字节,并放弃编译代码中的数据对齐的决定,反映了对紧凑性的有意偏好,可能会在简单的实现中牺牲一些性能。一个字节的操作码也限制了指令集的大小。不假设数据对齐意味着在许多机器上,比一个字节大的立即数必须在运行时从字节构建。
2.11.1. Types and the Java Virtual Machine
Java虚拟机指令集中的大多数指令都编码了它们执行的操作的类型信息。例如,iload指令(§iload)将一个本地变量的内容加载到操作数栈上,该变量必须是int类型。fload指令(§fload)也是如此,但是它加载的是float类型的值。这两个指令可能具有相同的实现,但具有不同的操作码。
对于大多数有类型的指令,指令类型在操作码助记符中由一个字母明确表示:i表示int操作,l表示long,s表示short,b表示byte,c表示char,f表示float,d表示double,a表示reference。一些类型明确的指令在助记符中没有类型字母。例如,arraylength总是操作一个数组对象。一些指令,如无条件控制转移指令goto,不操作有类型的操作数。
由于Java虚拟机的一字节操作码大小,将类型编码到操作码中会给其指令集的设计带来压力。如果每个有类型的指令都支持Java虚拟机的所有运行时数据类型,那么将会有更多的指令无法用一个字节表示。因此,Java虚拟机的指令集为某些操作提供了较低级别的类型支持。换句话说,指令集是故意不正交的。必要时可以使用单独的指令在不支持的数据类型和支持的数据类型之间进行转换。
指令包括几类:
Load and Store Instructions
Arithmetic Instructions
Type Conversion Instructions
Object Creation and Manipulation
Operand Stack Management Instructions
Control Transfer Instructions
Method Invocation and Return Instructions
Throwing Exceptions
Synchronization
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗