【问题标题】:Why does the Java compiler 11 use invokevirtual to call private methods?为什么Java编译器11使用invokevirtual调用私有方法?
【发布时间】:2021-07-17 10:31:56
【问题描述】:

使用 OpenJDK 8 中的 Java 编译器编译以下代码时,对 foo() 的调用是通过 invokespecial 完成的,但是当使用 OpenJDK 11 时,会发出 invokevirtual

public class Invoke {
  public void call() {
    foo();
  }

  private void foo() {}
}

使用javac 1.8.0_282 时javap -v -p 的输出:

  public void call();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #2      // Method foo:()V
         4: return

使用javac 11.0.10 时javap -v -p 的输出:

  public void call();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #2      // Method foo:()V
         4: return

我不明白为什么在这里使用invokevirtual,因为不能覆盖foo()

经过一番挖掘,似乎invokevirtual 在私有方法上的目的是允许嵌套类从外部类调用私有方法。所以我尝试了下面的代码:

public class Test{
  public static void main(String[] args) {
    // Build a Derived such that Derived.getValue()
    // somewhat "exists".
    System.out.println(new Derived().foo());
  }

  public static class Base {

    public int foo() {
      return getValue() + new Nested().getValueInNested();
    }

    private int getValue() {
      return 24;
    }

    private class Nested {

      public int getValueInNested() {
        // This is getValue() from Base, but would
        // invokevirtual call the version from Derived?
        return getValue();
      }
    }
  }

  public static class Derived extends Base {

    // Let's redefine getValue() to see if it is picked by the
    // invokevirtual from getValueInNested().
    private int getValue() {
      return 100;
    }
  }
}

用 11 编译这段代码,我们可以在javap 的输出中看到invokevirtualfoo()getValueInNested() 中都使用了:

  public int foo();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=4, locals=1, args_size=1
         0: aload_0

         // ** HERE **
         1: invokevirtual #2  // Method getValue:()I
         4: new           #3  // class Test$Base$Nested
         7: dup
         8: aload_0
         9: invokespecial #4  // Method Test$Base$Nested."<init>":(LTest$Base;)V
        12: invokevirtual #5  // Method Test$Base$Nested.getValueInNested:()I
        15: iadd
        16: ireturn
  public int getValueInNested();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #1  // Field this$0:LTest$Base;

         // ** HERE **
         4: invokevirtual #3  // Method Test$Base.getValue:()I
         7: ireturn

所有这些都有点令人困惑,并提出了一些问题:

  • 为什么invokevirtual用来调用私有方法?是否存在用 invokespecial 替换它的用例不等效?
  • Nested.getValueInNested() 中对getValue() 的调用如何不从Derived 中选择方法,因为它是通过invokevirtual 调用的?

【问题讨论】:

  • 由于JEP 181invokevirtualinvokeinterface可以分别调用类和接口的私有方法。这是为了避免使invokespecial 已经很重要的访问规则过于复杂。 invokevirtual 在性能方面并不比 invokespecial 好或差 - 它只是调用私有方法的一种现代方式,无论是同一个班级还是同胞。是的,可以将其替换回invokespecial - 甚至还有一个javac 标志:-XDdisableVirtualizedPrivateInvoke
  • @apangin 我感觉invokeinterface 指令(它作为一个整体存在),甚至可能在常量池中MethodrefInterfaceMethodref 之间的区别,甚至是不必要的并发症。 Java 8 中已经丢失了调用指令中接口方法和非接口方法之间的清晰分离,接口和嵌套类中的私有方法使其更加模糊。甚至在 Java 8 之前很久,解释器可以对这种区别进行的优化就变得无关紧要了。

标签: java java-8 jvm javac java-11


【解决方案1】:

这是作为https://openjdk.java.net/jeps/181:基于嵌套的访问控制的一部分完成的,因此 JVM 可以允许从嵌套类访问私有方法。

在此更改之前,编译器必须在嵌套类调用的Base 类中生成一个受包保护的合成方法。该合成方法将依次调用Base 类中的私有方法。 Java 11 中的功能增强了 JVM,无需编译器生成合成方法。

关于invokevirtual 是否会调用Derived 类中的方法,答案是否定的。私有方法仍然不受运行时类的method selection 约束(这一点从未改变):

在执行invokeinterfaceinvokevirtual 指令期间,选择方法涉及 (i) 堆栈上对象的运行时类型,以及 (ii)先前由指令解决的方法。关于类或接口 C 和方法 mR 选择方法的规则如下:

  1. 如果 mR 标记为 ACC_PRIVATE,则它是选定的方法。

编辑:

基于评论“如果私有方法是从方法所有者类调用的,是否仍然使用invokespecial,如果它是从嵌套类调用,则使用invokevirtual?”

正如 Holger 提到的,是的,它是有效的,但基于 JEP,我猜为了简单起见,我决定切换到 invokevirtual(虽然我无法证实这一点,这只是一个猜测):

随着访问规则的改变,以及对字节码规则的适当调整,我们可以允许生成调用字节码的简化规则:

  • 为私有的嵌套构造函数调用特殊函数,
  • 为私有非接口、nestmate 实例方法调用virtual,
  • invokeinterface 用于私有接口,nestmate 实例方法;和
  • 为私有嵌套对象调用静态方法,静态方法

JDK-8197445 : Implementation of JEP 181: Nest-Based Access Control 的另一个有趣的注释:

传统上,invokespecial 用于调用private 成员,尽管invokevirtual 也具有此功能。我们需要在不同的类中调用private 方法来使用invokevirtual,而不是扰乱invokespecial 强制执行的有关超类型的复杂规则。

【讨论】:

  • 但是对于调用者和方法在同一个类中的情况,不需要改变行为。
  • 如果私有方法是从方法所有者类调用的,仍然使用invokespecial,如果它是从嵌套类调用,使用invokevirtual是否有效?
  • @Julien 是的,the purpose of invokespecial hasn’t changed:“调用实例方法;直接调用实例初始化方法和当前类的方法及其超类型”。
【解决方案2】:

充值到一个已经很好的答案

认为在已经提供和接受的答案中添加更多信息是合适的,尽管这不是绝对必要的,但它可能有助于扩大理解,因此它符合 SO 用户的最大利益。

基于嵌套的访问控制

在 Java 11 之前的早期版本中,正如 @m-a 在接受的答案中已经指出的那样,编译器需要 创建桥接方法以允许类在这样的情况下访问彼此的私有成员 状况。这些可访问性扩展桥接方法在执行上下文中被调用,编译器将代码插入到正在运行的程序中。

这样做会增加已部署应用程序的大小并增加复杂性,而且更难理解幕后发生的事情。

Java 11 引入了基于嵌套的访问控制的概念。 除了 nestmates 的概念和 JVM 中相关的访问规则, 这允许类和接口相互嵌套。

嵌套类型可以是私有字段、方法和构造函数。

使用更新后的反射 API,您现在可以查询有关 基于嵌套的访问控制功能。

Java 11 中的一些新优点

getNestHost()方法用于获取嵌套主机名,isNestmateOf()方法 可以用来检查一个类是否是一个nestmate。 另外,getNestMembers() 方法返回一个嵌套成员数组。

这是一个通用示例的链接,由 Baeldung.com 提供,Nest Based Access Control 使得好处非常突出恕我直言。

请注意,反汇编代码中没有编译器生成的桥接方法。此外,Inner 类现在可以直接调用上面链接示例中的 outerPrivate() 方法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-10-27
    • 2013-11-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多