为什么Java的invokevirtual需要解析被调用方法的编译时类?

时间:2010-04-01 21:22:56

标签: java jvm methods virtual-method

考虑这个简单的Java类:

class MyClass {
  public void bar(MyClass c) {
    c.foo();
  }
}

我想讨论c.foo()行上会发生什么。

原创,误导性问题

注意:并非所有这些都发生在每个单独的invokevirtual操作码上。提示:如果您想了解Java方法调用,请不要只阅读invokevirtual的文档!

在字节码级别,c.foo()的内容将是invokevirtual操作码,并且根据the documentation for invokevirtual,或多或少会发生以下情况:

  1. 查找编译时类MyClass中定义的foo方法。 (这涉及首先解析MyClass。)
  2. 执行一些检查,包括:验证c不是初始化方法,并验证调用MyClass.foo不会违反任何受保护的修饰符。
  3. 找出实际调用的方法。特别是,查找c的运行时类型。如果该类型具有foo(),则调用该方法并返回。如果没有,查找c的运行时类型的超类;如果该类型具有foo,则调用该方法并返回。如果没有,查找c的运行时类型的超类的超类;如果该类型具有foo,则调用该方法并返回。等等..如果找不到合适的方法,那么错误。
  4. 单独的步骤#3似乎足以确定调用哪个方法并验证所述方法具有正确的参数/返回类型。所以我的问题是为什么第一步执行第一步。可能的答案似乎是:

    • 在步骤#1完成之前,您没有足够的信息来执行步骤#3。 (乍一看似乎难以置信,所以请解释一下。)
    • 在#1和#2中完成的链接或访问修饰符检查对于防止发生某些不良事件至关重要,必须根据编译时类型执行这些检查,而不是运行时类型层次结构。 (请解释。)

    修订问题

    c.foo()行的javac编译器输出的核心将是这样的指令:

    invokevirtual i
    

    其中i是MyClass'运行时常量池的索引。该常量池条目的类型为CONSTANT_Methodref_info,并将指示(可能是间接的)A)被调用方法的名称(即foo),B)方法签名,以及C)调用该方法的编译时类的名称on(即MyClass)。

    问题是,为什么需要编译时类型(MyClass)的引用?由于invokevirtual将在c的运行时类型上进行动态调度,因此将引用存储到编译时类不是多余的吗?

5 个答案:

答案 0 :(得分:4)

一切都与表现有关。通过计算编译时类型(又名:静态类型),JVM可以计算运行时类型的虚函数表中的调用方法的索引(也就是:动态类型)。使用该索引,步骤3简单地变成对阵列的访问,这可以在恒定时间内完成。不需要循环。

示例:

class A {
   void foo() { }
   void bar() { }
}

class B extends A {
  void foo() { } // Overrides A.foo()
}

默认情况下,A扩展Object,它定义了这些方法(通过invokespecial调用时省略了最终方法):

class Object {
  public int hashCode() { ... }
  public boolean equals(Object o) { ... }
  public String toString() { ... }
  protected void finalize() { ... }
  protected Object clone() { ... }
}

现在,考虑一下这个调用:

A x = ...;
x.foo();

通过确定x的静态类型为A,JVM还可以找出此调用站点可用的方法列表:hashCodeequals,{{1} },toStringfinalizeclonefoo。在此列表中,bar是第6个条目(foo是第1个,hashCode是第2个,等等)。索引的计算执行一次 - 当JVM加载类文件时。

之后,只要JVM进程equals只需要访问x提供的方法列表中的第6个条目,相当于x.foo(),(如果x.getClass().getMethods[5]则指向A.foo() x的动态类型是A)并调用该方法。无需穷举搜索这一系列方法。

请注意,无论x的动态类型如何,方法的索引都保持不变。即:即使x指向B的实例,第6种方法仍然是foo(虽然这次它将指向B.foo())。

<强>更新

[根据您的更新]:你是对的。为了执行虚方法调度,所有JVM需求都是方法的名称+签名(或vtable中的偏移量)。但是,JVM不会盲目地执行任何操作。它首先检查加载到其中的cassfiles在名为verification的过程中是否正确(另请参阅here)。

Verification表达了JVM的一个设计原则:它不依赖于编译器来生成正确的代码。它在允许代码执行之前检查代码本身。特别地,验证器检查每个调用的虚拟方法实际上是由接收器对象的静态类型定义的。显然,需要接收器的静态类型来执行这样的检查。

答案 1 :(得分:1)

在阅读文档后,这不是我理解的方式。我认为你已经转换了第2步和第3步,这将使整个系列事件更具逻辑性。

答案 2 :(得分:1)

据推测,编译器已经发生#1和#2。我怀疑至少部分目的是确保它们仍然保留在运行时环境中的类版本,这可能与编译代码的版本不同。

我还没有消化invokevirtual文档来验证你的摘要,所以Rob Heiser可能是对的。

答案 3 :(得分:1)

我在猜答案“B”。

  

在#1和#2中完成的链接或访问修饰符检查对于防止发生某些不良事件至关重要,并且必须根据编译时类型而不是运行时类型层次结构执行这些检查。 (请解释。)

5.4.3.3 Method Resolution描述了

#1,它进行了一些重要的检查。例如,#1在编译时类型中检查方法的可访问性,如果不是,则可能返回IllegalAccessError:

  

...否则,如果引用的方法无法访问(第5.4.4节)到D,则方法解析会抛出IllegalAccessError。 ...

如果你只检查了运行时类型(通过#3),那么运行时类型可能会非法扩大被覆盖方法的可访问性(例如,“坏事”)。确实编译器应该阻止这种情况,但是JVM仍然保护自己免受恶意代码的攻击(例如手工构造的恶意代码)。

答案 4 :(得分:0)

要完全理解这些内容,您需要了解方法解析在Java中的工作原理。如果您正在寻找深入的解释,我建议您查看“Java虚拟机内部”一书。第8章“链接模型”的以下部分可在线获取,看起来特别相关:

(CONSTANT_Methodref_info条目是类文件头中的条目,用于描述该类调用的方法。)

感谢Itay鼓励我做谷歌搜索所需要的。