【问题标题】:How are these Java byte offsets calculated?这些 Java 字节偏移量是如何计算的?
【发布时间】:2015-05-14 14:38:50
【问题描述】:

我有以下 Java 代码:

public int sign(int a) {
  if(a<0) return -1;
  else if (a>0) return 1;
  else return 0;
}

编译时生成以下字节码:

public int sign(int);
  Code:
     0: iload_1
     1: ifge          6
     4: iconst_m1
     5: ireturn
     6: iload_1
     7: ifle          12
    10: iconst_1
    11: ireturn
    12: iconst_0
    13: ireturn

我想知道字节偏移计数(第一列)是如何计算的,特别是当所有其他指令都是单字节指令时,为什么ifgeifle指令的字节计数是3字节?

【问题讨论】:

  • 第二个问题似乎很简单;偏移量占用空间。
  • 另请注意,此代码可能来自 (cory.li/bytecode-hacking) 的文章都解释了这一点,并提供了另一个链接以了解更多 JVM 内部结构 (blog.jamesdbloom.com/JVMInternals.html)。当然,如答案中所讨论的,JVM 规范是主要来源。虽然内容密集,但最终你会在那里学到比这里更多的东西。

标签: java jvm bytecode


【解决方案1】:

正如评论中已经指出的那样:ifgeifle 指令有一个额外的偏移量。

Java Virtual Machine Instruction Set specification for ifge and ifle 在此处包含相关提示:

格式

if<cond>
branchbyte1
branchbyte2

这表明有两个附加字节与该指令相关联,即“分支字节”。这些字节组合成单个short 值以确定偏移量 - 即当条件满足时指令指针应该“跳转”多远。


编辑:

cmets 让我感到好奇:offset 被定义为 有符号 16 位值,将跳转限制在 +/- 32k 范围内。这并不涵盖may contain up to 65535 bytes according to the code_length in the class file 的可能方法的全部范围。

所以我创建了一个测试类,看看会发生什么。这个类看起来像这样:

class FarJump
{
    public static void main(String args[])
    {
        call(0, 1);
    }

    public static void call(int x, int y)
    {
        if (x < y)
        {
            y++;
            y++;

            ... (10921 times) ...

            y++;
            y++;
        }
        System.out.println(y);
    }

}

y++ 的每一行都将被翻译成一个iinc 指令,由 3 个字节组成。所以得到的字节码是

public static void call(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: if_icmpge     32768
       5: iinc          1, 1
       8: iinc          1, 1

       ...(10921 times) ...

    32762: iinc          1, 1
    32765: iinc          1, 1
    32768: getstatic     #3             // Field java/lang/System.out:Ljava/io/PrintStream;
    32771: iload_1
    32772: invokevirtual #4             // Method java/io/PrintStream.println:(I)V
    32775: return

可以看到它仍然使用if_icmpge指令,偏移量为32768(编辑:这是一个绝对偏移量。相对偏移量是32766。另见this question)

通过在原代码中多加一个y++,编译后的代码突然变成了

public static void call(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: if_icmplt     10
       5: goto_w        32781
      10: iinc          1, 1
      13: iinc          1, 1
      ....
    32770: iinc          1, 1
    32773: iinc          1, 1
    32776: goto_w        32781
    32781: getstatic     #3             // Field java/lang/System.out:Ljava/io/PrintStream;
    32784: iload_1
    32785: invokevirtual #4             // Method java/io/PrintStream.println:(I)V
    32788: return

因此它将条件从if_icmpge 反转为if_icmplt,并使用goto_w 指令处理远跳转,该指令包含四个 分支字节,因此可以覆盖(超过)一个完整的方法范围。

【讨论】:

  • 你知道为什么偏移量(由 2 个分支字节组成一个 16 位值)被限制为 16 位大小吗?这种尺寸限制的含义是什么?
  • @jcm:可能是因为 16 位偏移量对于迄今为止大多数分支来说已经足够了。这当然意味着不能直接表示偶尔较长的分支。如果需要更长的分支,编译器可能会生成一个goto_w 或带有相反条件分支的东西。
  • @jcm 含义很明显;你只能跳转 64K 字节。这很有效,因为如果您遵循 JVM 规范,您会发现一个方法的最大值为... 64K。
  • @DaveNewton:带符号的 16 位偏移量只能跳过 32k 条指令。因此,如果您在“if () { ... }”中包装了一个疯狂的 >32k 字节码方法,那么普通的条件分支将无法使用。
  • @MattiVirkkunen 猜对了对面的分支和goto_w - 查看编辑。
【解决方案2】:

字节偏移量可以很容易地通过将之前每条指令的大小相加来计算。指令大小记录在JVM specs

if&lt;cond&gt; 指令比其他指令占用更多空间,因为除了单字节操作码之外,它们还有两个额外字节用于指定条件为真时要跳转到的偏移量。

如果您想进一步试验,例如,您可以尝试在代码中使用更大的常量(例如 20)。您会看到加载这些指令的指令也将使用额外的字节来存储常量值。为了提高效率,常用的小数字有一个字节的编码(例如iconst_1)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-04-10
    • 1970-01-01
    • 2015-03-15
    • 1970-01-01
    • 2018-04-13
    • 1970-01-01
    • 2017-02-26
    • 2012-10-24
    相关资源
    最近更新 更多