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

static int giveMeThatOldFashionedBoolean(boolean bVal) { try { if (bVal) { …
关注者
35
被浏览
16,343

1 个回答

首先,如果题主只关心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了,代价则是字节码大小会膨胀…

为什么?