Java字节码由JVM按顺序执行



我对Java字节码很陌生。根据我的理解,当分解JAR文件时,结果将是JVM直接解释的字节码(数字)。每个字节或2个字节的数字都与实际Java源文件中的Java方法相关联。我在哪里可以找到这些地图?

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

提前谢谢。

理解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的值并将其推入堆栈(完整的指令列表)。在非静态方法中,局部变量0始终是this参数,因此在指令0之后,我们的堆栈是

| this |
+------+

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

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

下一个指令CCD_ 6是执行分配CCD_。此指令从堆栈中弹出两个值。第一个弹出的值是字段的新值。第二个弹出的值是指向保存要分配的字段的对象的指针。要分配的域的名称被编码在指令中(这就是为什么指令需要三个字节:它从位置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指定要分配的字段是p1.Movie类的price字段(如您所见,#18引用#1、#19,后者又引用#5和#6。分配给字段的实际名称显示在常量池中)

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

评估堆栈现在为空。

最后一条指令简单地返回。

这里还不完全清楚你在问什么,所以让我回答一些问题:

方法边界是定义良好的,不同于"正常"的程序集代码。类型在任何地方都定义良好。字段定义良好。类是定义良好的。指令边界是明确定义的(跳到指令中间是非法的)。代码和数据很容易区分。方法不能访问彼此的变量;仅字段。这些东西使分析Java字节码比分析机器代码容易得多。

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

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

以下说明影响控制流程:

  • areturndreturnfreturnireturnlreturn—执行时,使方法正常返回。在极少数情况下(错误生成的字节码),也可以抛出IllegalMonitorStateException
  • athrow—执行时会引发异常
  • if_icmpneif_icmpeqif_icmpltif_icmpgeif_icmpgtif_icmpleifneifeqifltifgeifgtifleifnonnullifnull—条件跳转指令
  • gotogoto_w—无条件跳转指令
  • aaloadaastoreanewarrayarraylengthbaloadbastorecaloadcastorecheckcastdaloaddastorefaloadfastoregetfieldgetstaticialoadiastoreidivinstanceofputfield #180、laloadlastoreldcldc_wldc2_wldivlremmonitorentermonitorexitthis.price = price0、newarrayputfieldputstaticsaloadsastore-在某些情况下可以抛出异常
  • invokedynamicinvokeinterfaceinvokespecialinvokestaticinvokevirtualnew-导致调用其他方法,这可能导致引发异常
  • jsrjsr_wret-允许方法包含从多个位置调用的子例程。幸运的是,现代编译器似乎不会生成这些

正如EJP在注释中所说,您可以反编译一个名为example的类。通过使用javap-c示例Main。Main.class命令。正如我们所看到的,Java字节码比IA32更结构化。实际的字节码指令包含在方法中,就像Java本身一样。

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

  1. JVM规范
  2. 维基百科

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或引用类型)的变量将存储在单个槽中。但是,long或double类型的变量需要两个插槽来存储它们。此外,在非静态方法中,时隙#0总是用于保持this。此外,参数总是与编号最低的插槽相关联。因此,在非静态方法moo(String message, int times)中,this将在时隙#0中,变量message将在时隙#1中,而变量times将在时隙#2中。

为了确定方法的字节码中局部变量alive的位置,您需要对方法的字节代码使用Live variable Analysis,因为字节码指令是随后执行的(即不是一次全部执行),但不一定是线性执行的。

另一方面,字段不存储在堆栈帧中的上述字段中。我认为您可能指的是字段,因为它们可以"在类中初始化",然后在不同的方法中使用,而局部变量是声明它们的方法的局部变量。

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

不过,您似乎关心的主要问题是字节码平台的结构问题。

  • 不,字节码不能跳转到另一个函数。转移控制的唯一方法是通过异常或特殊调用指令,这些指令通过VM。每个堆栈帧都是完全隔离的。

  • 此外,所有字节码都强制执行了类型检查,尽管类型检查比Java语言级别的检查要宽松一些。例如,您不能将浮点值作为int进行插入,更不用说指针了。所有内存访问都被虚拟机抽象掉了,不可能像在本机代码中那样进行原始内存访问。

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

最新更新