Java字节码是否由JVM顺序执行?

时间:2014-10-31 05:57:57

标签: java bytecode

我是Java字节码的新手。根据我的理解,在反汇编JAR文件时,结果将由JVM直接解释字节码(数字)。每个字节或2个字节的数字与实际Java源文件中的Java方法相关联。我在哪里可以找到这些的映射?

此外,假设我想知道变量是否在类中初始化但从未再次使用过。我可以简单地检查它何时被实例化,然后认为如果在初始化之后它再次出现在字节码中它从未使用过它?为了使这个逻辑起作用,JVM必须按顺序执行字节码,以便初始化变量不能跳转到另一个函数,等等。定义的函数边界与通用汇编代码(intel,MIPS)不同。

提前致谢。

4 个答案:

答案 0 :(得分:3)

我不清楚你在这里问的是什么,所以让我回答一些事情:

方法边界是明确定义的,不像" normal"汇编代码。类型在各处都有明确的定义。字段定义明确。类是明确定义的。指令边界是明确定义的(跳转到指令的中间是非法的)。代码和数据很容易区分。方法不能互相访问#39;变量;只有字段。这些事情使得 更容易分析Java字节码而不是机器代码。

要从Java程序读取和写入类文件,我建议使用ASM library。它将负责理解类文件格式,并将其转换为更易于使用的格式(Java对象树或方法调用序列)。还有其他具有类似用途的库,例如BCEL,cgLib和Javassist。我不熟悉那些其他库来比较它们。

方法中的字节码按顺序执行,对于"大多数"说明。有几条指令可能导致执行不顺序 - 通常这是指令的意图(例如条件跳转,用于实现if / while / etc)。许多指令也可以抛出异常,这会导致执行跳转到异常处理程序或退出当前方法。

以下说明会影响控制流程:

  • areturndreturnfreturnireturnlreturn - 执行时,会导致方法正常返回。在极少数情况下(错误生成的字节码)也可以抛出IllegalMonitorStateException
  • athrow - 执行时会导致抛出异常。
  • if_icmpneif_icmpeqif_icmpltif_icmpgeif_icmpgtif_icmpleifneifeqifltifgeifgtifleifnonnullifnull - 条件跳转说明
  • gotogoto_w - 无条件跳转说明
  • aaloadaastoreanewarrayarraylengthbaloadbastorecaloadcastorecheckcastdaloaddastorefaloadfastoregetfieldgetstaticiaload,{ {1}},iastoreidivinstanceofiremlaloadlastoreldc,{{1 }},ldc_wldc2_wldivlremmonitorentermonitorexitmultianewarraynewarrayputfieldputstatic - 在某些情况下可以抛出异常。
  • saloadsastoreinvokedynamicinvokeinterfaceinvokespecialinvokestatic - 导致调用其他方法,这可能会导致抛出异常。
  • invokevirtualnewjsr - 允许方法包含从多个地方调用的子程序。幸运的是,现代编译器似乎没有生成这些。

答案 1 :(得分:2)

理解JVM字节码需要一些时间。为了帮助您入门,您需要了解两件事:

  • JVM是一个堆栈机器:当它需要计算一个表达式时,它首先将表达式的输入推送到一个堆栈中,然后对表达式的评估实质上是从堆栈中弹出所有输入并将结果推回进入堆栈的顶部。反过来,这个结果可以用作另一个表达式评估的输入。

  • 所有参数和局部变量都存储在局部变量数组中。

让我们在实践中看到这一点。这是一个源代码:

package p1;

public class Movie {
  public void setPrice(int price) {
    this.price = price;
  }
}

正如EJP所说,您应该运行javap -c,以查看字节码:javap -c bin/p1/Movie.class。这是输出:

public class p1.Movie {
  public p1.Movie();
    Code:
       0: aload_0       
       1: invokespecial #10   // Method java/lang/Object."<init>":()V
       4: return        

  public void setPrice(int);
    Code:
       0: aload_0       
       1: iload_1       
       2: putfield      #18    // Field price:I
       5: return        
}

查看输出,您可以看到在字节码中我们看到了默认构造函数和setPrice方法。

第一条指令aload_0获取局部变量0的值并将其推入堆栈(complete list of instructions)。在非静态方法中,局部变量0始终是this参数,因此在指令0之后我们的堆栈是

| this |
+------+

下一条指令是aload_1,它接受​​局部变量1的值并将其推入堆栈。在我们的局部变量1中是方法的参数(价格)。我们的堆栈现在看起来如下:

| price |
| this  |
+-------+

下一条指令putfield #18是执行作业this.price = price的指令。该指令从堆栈中弹出两个值。第一个弹出值是字段new value。第二个弹出值是指向包含要分配的字段的对象的指针。要分配的字段的名称在指令中编码(这就是指令占用三个字节的原因:它从位置2开始,但下一条指令从位置5开始)。编码到指令中的额外值是“#18”。这是常量池的索引。要查看常量池,您应该运行:javap -v bin/p1/Movie.class

Classfile /home/imaman/workspace/Movie-shop/bin/p1/Movie.class
...
Constant pool:
   #1 = Class              #2             //  p1/Movie
   ...
   #5 = Utf8               price
   #6 = Utf8               I
   ...
   #18 = Fieldref          #1.#19          //  p1/Movie.price:I
   #19 = NameAndType       #5:#6           //  price:I
   ...

因此#18指定要分配的字段是price类的p1.Movie字段(正如您所见,#18引用了#1,#19,其中,转向参考#5和#6。分配给字段的实际名称出现在常量池中

回到我们执行的putfield指令:从堆栈弹出两个值后,JVM现在将第一个弹出值分配给price字段(由#18表示) this对象(第二个弹出值)。

评估堆栈现在为空。

最后一条指令只是返回。

答案 2 :(得分:1)

正如EJP在评论中所述,您可以使用javap -c example.Main.class命令对名为example.Main的类进行反编译。可以看出,Java字节码远比IA32更有条理。实际的字节码指令包含在方法中,就像在Java本身中一样。

您可以在以下位置找到有关每个字节码指令的信息:

  1. JVM Specification
  2. Wikipedia
  3. JVM指令操作一堆操作数。例如:

    LDC 10    // Push the constant 10 onto the stack.
    LDC 20    // Push the constant 20 onto the stack.
    IADD      // Pop two numbers off the stack, add them, push the result.
    ISTORE 5  // Pop an integer (in this case 30) off the stack and put it in variable #5.
    

    您可能会注意到,局部变量实际上存储在堆栈帧的编号插槽中。 Java编译器的工作是将局部变量与编号槽相关联。重要的是要指出类型(boolean,char,byte,short,int或reference-type)的变量将存储在单个插槽中。但是,long或double类型的变量需要两个插槽来存储它们。此外,在非静态方法中,插槽#0始终用于保存this。此外,参数始终与编号最小的槽相关联。因此,在非静态方法moo(String message, int times)中,this将位于插槽#0中,变量message将位于插槽#1和变量times中将在插槽#2中。

    为了确定方法的字节码中局部变量 alive 的位置,你需要在方法的字节码上使用Live Variable Analysis,因为字节码指令随后被执行(即不是一次性的,但不一定是线性的。

    另一方面,字段未存储在上述堆栈帧中。我认为你可以引用字段,因为它们可以“在类中初始化”然后用于不同的方法,而局部变量是声明它们的方法的本地变量。

答案 3 :(得分:0)

JVM不一定按顺序执行字节码,但它的行为就像它一样(有点像处理器上的乱序执行,但深度优化更多)。

您似乎关注的主要问题是字节码平台的结构性问题。

  • 不,字节码无法跳转到其他功能。传输控制的唯一方法是通过异常或特殊呼叫指令,它们通过VM。每个堆栈框架都是完全隔离的。

  • 此外,所有字节码都强制执行类型检查,尽管类型检查比Java语言级别更宽松。你不能把它作为一个浮点数并将它作为一个int进行插入,更不用说指针了。所有内存访问都被VM抽象出来,无法像在本机代码中那样进行原始内存访问。

  • 指令可以超过2个字节(实际上,切换指令可以是任意长的)。但是绝大多数指令都是1或3个字节。它们不一定与Java源的元素1对1对应,尽管映射通常很简单。通常,Java的更高版本会添加更多的语法糖,这会降低使用这些功能时字节码与原始源代码的相似性(一个值得注意的情况是切换字符串)。