JVM的任何编译器都使用“宽” goto吗?


47

我知道大多数人都知道这goto是Java语言中的保留关键字,但实际上并未使用。您可能还知道这goto是Java虚拟机(JVM)操作码。我认为所有的Java,Scala和科特林的复杂的控制流结构,在JVM的水平,使用的某种组合来实现gotoifeqifleiflt,等。

查看JVM规范https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.goto_w我看到还有一个goto_w操作码。而goto采用2字节的分支偏移量,goto_w则采用4字节的分支偏移量。规范指出

尽管goto_w指令采用4字节的分支偏移量,但其他因素将方法的大小限制为65535字节(第4.11节)。在Java虚拟机的未来版本中可能会提高此限制。

在我看来goto_w,就像其他一些*_w操作码一样,它是面向未来的。但我也goto_w想到,也许可以将两个较高有效字节清零,并将两个较低有效字节与for相同goto,并根据需要进行调整。

例如,给定以下Java Switch-Case(或Scala Match-Case):

     12: lookupswitch  {
                112785: 48 // case "red"
               3027034: 76 // case "green"
              98619139: 62 // case "blue"
               default: 87
          }
      48: aload_2
      49: ldc           #17                 // String red
      51: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      54: ifeq          87
      57: iconst_0
      58: istore_3
      59: goto          87
      62: aload_2
      63: ldc           #19                 // String green
      65: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      68: ifeq          87
      71: iconst_1
      72: istore_3
      73: goto          87
      76: aload_2
      77: ldc           #20                 // String blue
      79: invokevirtual #18 
      // etc.

我们可以将其重写为

     12: lookupswitch  { 
                112785: 48
               3027034: 78
              98619139: 64
               default: 91
          }
      48: aload_2
      49: ldc           #17                 // String red
      51: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      54: ifeq          91 // 00 5B
      57: iconst_0
      58: istore_3
      59: goto_w        91 // 00 00 00 5B
      64: aload_2
      65: ldc           #19                 // String green
      67: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      70: ifeq          91
      73: iconst_1
      74: istore_3
      75: goto_w          91
      79: aload_2
      81: ldc           #20                 // String blue
      83: invokevirtual #18 
      // etc.

我实际上没有尝试过,因为更改“行号”以容纳goto_ws 可能犯了一个错误。但是由于它在规范中,所以应该可以做到。

我的问题是,goto_w除了表明可以做到这一点之外,是否还有其他原因可以与当前65535限制一起使用编译器或其他字节码生成器?

Answers:


51

方法代码的大小可以最大为64K。

短路的分支偏移goto是带符号的16位整数:从-32768到32767。

因此,短偏移量不足以使从65K方法的开头跳到结尾。

甚至javac有时会散发出来goto_w。这是一个例子:

public class WideGoto {

    public static void main(String[] args) {
        for (int i = 0; i < 1_000_000_000; ) {
            i += 123456;
            // ... repeat 10K times ...
        }
    }
}

反编译javap -c

  public static void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: iload_1
       3: ldc           #2
       5: if_icmplt     13
       8: goto_w        50018     // <<< Here it is! A jump to the end of the loop
          ...

// ... repeat 10K times ...编译吗?我知道单个源类的大小是有限制的……但是我不知道它到底是什么(代码生成是我唯一见到过的东西)。
Elliott Frisch

3
@ElliottFrisch确实如此。只要该方法的字节码大小不超过65535,常量池长度也小于65535
apangin

18
凉。谢谢。我猜64k应该足够了。;)
Elliott Frisch

3
@ElliottFrisch- 参考提示技巧。
TJ Crowder

34

goto_w当分支适合时,没有理由使用goto。但是您似乎错过了使用有符号偏移量的分支是相对分支的原因,因为分支也可以向后移动。

在查看工具(如)的输出时,您不会注意到它javap,因为它会在打印前计算最终的绝对目标地址。

因此goto,的范围-327678 … +32767‬并不总是足以解决该0 … +65535范围内的每个可能的目标位置。

例如,以下方法将goto_w在开头有一条指令:

public static void methodWithLargeJump(int i) {
    for(; i == 0;) {
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        } } } } } } } } } } } } } } } } } } } } 
    }
}
static void x() {}

关于Ideone的演示

Compiled from "Main.java"
class LargeJump {
  public static void methodWithLargeJump(int);
    Code:
       0: iload_0
       1: ifeq          9
       4: goto_w        57567

7
哇真厉害 我最大的Java项目(其中包含一些程序包和几十个类)可编译为将近200KB。但是您MainmethodWithLargeJump()编译到将近400KB。
阿隆索阿尔特

4
这说明了针对常见情况优化了多少Java……
Holger

1
您是如何发现滥用跳表的?机器生成的代码?
Elliott Frisch

14
@ElliottFrisch我只需要记住,finally对于正常和异常的流程,块会重复(从Java 6开始是必需的)。因此,嵌套其中的十个意味着×2¹⁰,那么switch始终具有默认目标,因此与iload一起,它需要十个字节加填充。我还在每个分支中添加了一个平凡的语句来防止优化。利用限制是一个经常出现的话题,包括嵌套表达式lambdas字段构造函数 ……
Holger

2
有趣的是,嵌套表达式和许多构造函数也遇到了编译器实现方面的限制,而不仅仅是字节码限制。还有关于最大类文件大小的问答(也许我在编写此答案时不自觉地想起了塔吉尔的答案)。最后是最大包名称长度,在JVM端,最大嵌套已同步。似乎,人们一直保持好奇心。
霍尔格

5

目前看来,在一些编译器(在1.6.0和11.0.7试过),如果一个方法是足够大的永远需要goto_w,它采用专门 goto_w。即使它具有非常局部的跳转,它仍然使用goto_w。


1
为什么会这样呢?与指令缓存有关吗?
亚历山大-恢复莫妮卡

@ Alexander-ReinstateMonica可能只是易于实现。
David G.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.