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