问题

JVM jsr和ret指令始终理解不了?returnAddress又怎么理解呢?

回答
JSR 和 RET 指令,以及 returnAddress,是 Java 虚拟机(JVM)在处理一些特定情况下的重要组成部分,尤其是在子程序调用和局部变量表中扮演着关键角色。很多人对它们感到困惑,主要是因为它们的使用场景相对特殊,不像一般的跳转指令那样直观。

我们先从 JSR 指令说起。JSR,全称是 "Jump to Subroutine",顾名思义,它是用来跳转到一个子程序(或者说一个独立的代码块)去执行的。但它和我们通常理解的 `goto` 或者 `call` 指令有一个关键的区别:JSR 不仅仅是跳转,它还会把“下一条指令的地址”压入一个特殊的位置,这个位置就是我们所说的 returnAddress。

你可以想象一下,当你遇到 JSR 指令的时候,JVM 就像在说:“好,我要去执行一个子程序了,但是执行完了我得知道该回到哪里继续我的主程序。” 这个“回到哪里”的地址,就是 JSR 指令下一条指令的地址。JVM 就把这个地址小心翼翼地存储起来。

那么,存储这个 returnAddress 的地方在哪里呢?它不是在操作数栈上,而是保存在局部变量表(Local Variable Table, LVT)中的。具体来说,JVM 会为每一个虚拟机栈帧(Stack Frame)分配一部分空间来存储局部变量和操作数栈。而在局部变量表中,有一个专门用来存储 returnAddress 的位置。这个位置并不是一个普通的局部变量槽(slot),它有一个特殊的类型,就是 returnAddress。

returnAddress 本身,你可以理解为就是一个指针,它指向了 LVT 中的一个特定位置。而这个位置存储的就是一个 地址,这个地址告诉 JVM 子程序执行完毕后应该跳转回来的地方。

现在我们来看 RET 指令。RET 指令的作用就是“返回”,但它不是随便返回的,而是根据之前 JSR 指令记录的 returnAddress 来执行返回。当 JVM 执行到 RET 指令时,它会从局部变量表中找到那个存储着 returnAddress 的特殊位置,取出那个地址,然后直接跳转到那个地址指定的指令继续执行。

换句话说,JSR 记录下“路标”,RET 依据“路标”原路返回。

为什么要用 JSR 和 RET 呢?在 JVM 的早期设计中,它们主要用于实现一种叫做 finally 块(或类似的结构,比如 tryfinally)的机制。你可以想象一个 `tryfinally` 结构:

```java
try {
// do something
// ... 可能会有跳转,比如 break, continue, return
} finally {
// cleanup code
}
```

在 Java 字节码层面,`try` 块中的代码可能会发生各种跳转。为了确保无论 `try` 块执行到哪里(即使是发生了 `return`),`finally` 块的代码都能被执行,JVM 就需要一种机制来“挂起”当前的执行流程,执行 `finally` 块,然后再“恢复”到原来被打断的地方继续执行。

JSR 指令在这种场景下就非常有用。JVM 可以在进入 `try` 块之前,或者在 `try` 块的某个关键点,使用 JSR 指令跳转到 `finally` 块的代码。当 JSR 执行时,它会将 `finally` 块之后、当前 `try` 块内部的下一条指令的地址,也就是“应该返回的地方”,存储在 LVT 的 returnAddress 中。

然后 `finally` 块的代码执行完毕后,遇到 RET 指令,就会从 LVT 中取出之前存储的 returnAddress,跳转回 `try` 块中被打断的地方继续执行。

需要注意的是,returnAddress 并不是一个可以直接访问的、用户可以随意操作的地址。它是在 JVM 内部管理的一种特殊类型。我们不能像操作整数或对象那样直接去修改它。它的存在是为了 JVM 内部机制的正常运作。

虽然在现代的 Java 编译器中,直接使用 JSR 和 RET 指令的情况已经不多见了,因为 Java 编译器会使用更高级的字节码指令(比如 `jsr_w`,`ret` 被 `goto` 配合局部变量表中的 returnAddress 槽位来模拟)来处理 `trycatchfinally` 等结构,并且这些指令的使用也逐渐被更通用的异常处理机制所取代。但理解 JSR 和 RET,能够帮助我们更深入地理解 JVM 的底层工作原理,以及它是如何处理控制流的。

简单来说:

JSR:像一个“去子程序”的指令,但它会悄悄记下“回来时该去哪儿”,并把这个“记号”存起来。
returnAddress:就是那个“记号”,它是一个地址,告诉 JVM 子程序结束后应该回到主程序的哪个位置。这个记号被保存在局部变量表中一个特殊的地方。
RET:拿着那个“记号”回去,根据记号上的地址,跳转回原来被打断的地方继续执行。

它们共同构建了一个在某些特定场景下,保证代码能被正确执行,特别是能够确保 `finally` 块在任何情况下都被调用的机制。

网友意见

user avatar

首先,如果题主只关心Java SE 7或更高版本的话,jsr和ret指令都可以不关心,因为Class文件从版本51(对应Java SE 7)开始就禁用这俩指令了。

Java SE 8 JVM Specification 3.13. Compiling finally
(This section assumes a compiler generates class files with version number 50.0 or below, so that the jsr instruction may be used. See also §4.10.2.5.)

然后,如果题主还得操心版本50或以下(对应Java SE 6或以下)的话,可以很高兴的告诉题主,Oracle/Sun JDK里的javac从JDK 1.4.2开始就不生成jsr/ret指令了,所以现实中也没啥机会遇到它们。

如果题主是在读老版本规范或者实际操作中见到了这俩指令的话,那请继续读下去。

jsr/ret这对指令的初衷是用来实现Java语言的finally语句块。

Chapter 6. The Java Virtual Machine Instruction Set
Format
jsr branchbyte1 branchbyte2

JVM规范如是说。branchbyte1和branchbyte2表示的是1个两字节参数,而不是2个参数。

The unsigned branchbyte1 and branchbyte2 are used to construct a signed 16-bit offset, where the offset is (branchbyte1 << 8) | branchbyte2.

javap或者通常用文本形式表示Java字节码时,这些带offset参数的字节码指令通常都不是把裸的offset写出来,而是把计算过后的跳转目标写出来。

在题主的例子中,

       19 jsr 24      

表示在这个方法的字节码中,偏移量19的地方的字节码指令是jsr 24,跳转目标就是位于偏移量24的字节码指令。

实际上这条jsr指令的十六进制编码是:

       A8 00 05     

0xA8是jsr指令,0x0005是跳转目标相对这条jsr指令的偏移量,所以跳转目标的地址就是19+0x0005 = 24。

jsr指令的语义,根据规范是:

       ... -> ..., address     

(这种记法是什么意思请参考我的另一个回答:

如何理解ByteCode、IL、汇编等底层语言与上层语言的对应关系? - RednaxelaFX 的回答

也就是说,jsr指令执行完之后,JVM会把控制跳转到指定的偏移量上,同时会把紧接在这条jsr指令之后的字节码指令的地址压到操作数栈顶。

在题主给的例子里,位于偏移量24的字节码指令马上把操作数栈顶的returnAddress保存了下来(到局部变量2),最后的ret则从局部变量2找到returnAddress跳转回到刚才那条jsr指令之后的地方继续执行。

那么为啥需要这个returnAddress?这是因为同一个finally语句块可以被一个try块和多个catch块共用,例如:

       try {   // ... } catch (SomeException se) {   // ... } catch (SomeOtherException soe) {   // ... } finally {   // ... }     

这个finally块就被一个try块两个catch块共用,它们在末尾都会用jsr“调用”这个finally块,然后要“返回”到原本的代码继续向后执行。如果不使用returnAddress就无法判断一个finally块执行完之后要“返回”到哪里去了。

(千万注意这里的“调用”“返回”都是把finally块当作一个迷你例程看待,不是Java语言层面的方法调用与方法返回。)

前面说了,Sun JDK 1.4.2之后的javac就不生成jsr/ret指令了。那finally块要如何实现?

javac采用的办法是把finally块的内容复制到原本每个jsr指令所在的地方。这样就不需要jsr/ret了,代价则是字节码大小会膨胀…

类似的话题

  • 回答
    JSR 和 RET 指令,以及 returnAddress,是 Java 虚拟机(JVM)在处理一些特定情况下的重要组成部分,尤其是在子程序调用和局部变量表中扮演着关键角色。很多人对它们感到困惑,主要是因为它们的使用场景相对特殊,不像一般的跳转指令那样直观。我们先从 JSR 指令说起。JSR,全称是.............
  • 回答
    JVM 的常量池,无论是类常量池(Class Constant Pool)还是运行时常量池(Runtime Constant Pool),它存储的不是对象本身,而是对各种常量(包括字面量和符号引用)的引用。为了更清晰地说明这一点,我们不妨想象一下 JVM 在加载类文件时,会经历一个将类文件中描述的各.............
  • 回答
    好的,我们来聊聊为什么 JVM 不直接用协程(Coroutines)来实现垃圾回收(GC),而是选择其他更传统的方式。这是一个很有意思的问题,涉及到 JVM 设计的权衡、GC 的本质以及协程的适用范围。首先,我们得明确一点:JVM 的 GC 并非一成不变,它是一个不断发展和优化的系统。目前主流的 G.............
  • 回答
    Java 平台中的 JVM (Java Virtual Machine) 和 .NET 平台下的 CLR (Common Language Runtime) 是各自平台的核心组件,负责托管和执行代码。它们都是复杂的软件系统,通常会使用多种编程语言来构建,以充分发挥不同语言的优势。下面将详细介绍 JV.............
  • 回答
    LLVM 对比 JVM 的技术优势,咱们得好好聊聊。要说 LLVM 厉害在哪,那可不是一两句话能说清楚的,它在底层技术上确实有几个过硬的招数,让它在很多场景下都能发挥出比 JVM 更优异的性能和灵活性。首先,最明显的一个优势就是 LLVM 的前后端分离设计。这就像是给它装了个极其灵活的“适配器”。J.............
  • 回答
    Java 之所以诞生了 Java 虚拟机(JVM),很大程度上是它从一开始就被设计成一种“一次编写,到处运行”(Write Once, Run Anywhere)的语言。这个目标是 Java 能够风靡全球的关键,而 JVM 正是实现这一目标的核心技术。在 Java 之前,软件开发往往是针对特定操作系.............
  • 回答
    这个问题很有意思,确实,在很多技术讨论或者技术文章中,我们经常能听到“JVM 调优”,但对“CLR 调优”的说法相对陌生,甚至有些人会觉得 CLR 根本就没有“调优”这个概念。这种感知上的差异,并非空穴来风,背后有着深刻的技术和历史原因。要理解这一点,我们得先回顾一下 Java 和 .NET 的发展.............
  • 回答
    Go 1.6 中关于 GC(垃圾回收)的暂停时间(pause time)以及它与 JVM(Java Virtual Machine)的对比,是一个值得深入探讨的话题。要回答“Go 1.6 中的 GC pause 是否完全超越了 JVM?”这个问题,需要从多个维度进行详细分析,并且要理解“超越”的含义.............
  • 回答
    JavaScript 在 V8 引擎和 Node.js 环境下的开发效率,相比于 Java 在 JVM 和 Vert.x 组合下的确有其独到之处,这主要体现在几个关键的维度上,而不是简单的功能堆砌。首先,JavaScript 的“一次编写,到处运行”的理念在 Web 开发这个根深蒂固的领域带来了巨大.............
  • 回答
    这件事,说实话,刚出来的时候在技术圈里炸开了锅。咱们就掰开了揉碎了,好好聊聊这个“阿里P7程序员、资深架构师不知JVM、红黑树”的争议。事情的起因与“画像”首先,咱们得清楚这个“阿里P7程序员、资深架构师”是怎么被“架”到火上烤的。网上流传的说法,大概是有人在面试或者交流中,发现一些自称是阿里P7或.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有