为什么Java编译器11使用invokevirtual来调用私有方法?

Jul*_*ien 51 java jvm javac java-8 java-11

When compiling the code below with the Java compiler from OpenJDK 8, the call to foo() is done via an invokespecial, but when OpenJDK 11 is used, an invokevirtual is emitted.

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

  private void foo() {}
}
Run Code Online (Sandbox Code Playgroud)

Output of javap -v -p when javac 1.8.0_282 is used:

  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
Run Code Online (Sandbox Code Playgroud)

Output of javap -v -p when javac 11.0.10 is used:

  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

Run Code Online (Sandbox Code Playgroud)

I don't get why invokevirtual is used here since there cannot be an override of foo().

After digging a bit it seems that the purpose of invokevirtual on private methods is to allow nested classes to call private methods from the outer class. So I tried the code below:

  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
Run Code Online (Sandbox Code Playgroud)

Compiling this code with 11, we can see in the output of javap that invokevirtual is used both in foo() and in 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
Run Code Online (Sandbox Code Playgroud)
  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
Run Code Online (Sandbox Code Playgroud)

All of this is a bit confusing and raises some questions:

  • Why invokevirtual is used to call private methods? Is there a use case where replacing it by an invokespecial would not be equivalent?
  • How does the call to getValue() in Nested.getValueInNested() not pick the method from Derived since it is called via invokevirtual?

man*_*uti 48

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

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

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

期间的执行invokeinterfaceinvokevirtual指令,一种方法是选择相对于(i)所述栈上的对象的运行时类型,和(ii)一种方法,先前由指令解析。相对于类或接口 C 和方法 m R选择方法的规则如下:

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

编辑:

基于评论“如果从方法所有者类调用私有方法并且使用 invokevirtual 如果从嵌套类调用,那么仍然使用 invokespecial 是否有效?”

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

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

  • invokespecial 用于私有嵌套构造函数,
  • invokevirtual 用于私有非接口,嵌套实例方法,
  • 私有接口的调用接口,嵌套实例方法;和
  • invokestatic 用于私有嵌套,静态方法

JDK-8197445 的另一个有趣的注释:JEP 181 的实现:基于嵌套的访问控制

传统上,invokespecial是用来调用private成员的,虽然invokevirtual也有这个能力。与其干扰由 强制执行的有关超类型的复杂规则invokespecial,我们需要调用private不同类中的方法来使用invokevirtual

  • 但是,对于调用者和方法位于同一类中的情况,无需更改行为。 (4认同)
  • @Julien是的,[`invokespecial`的目的没有改变](https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html#jvms-6.5.invokespecial): “调用实例方法;直接调用实例初始化方法和**当前类**及其超类型的方法”。 (4认同)

cse*_*der 9

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

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

基于 Nest 的访问控制

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

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

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

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

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

Java 11 中的一些新优点

getNestHost()方法用于获取嵌套主机的名称,该isNestmateOf()方法可用于检查类是否为嵌套对象。此外,getNestMembers()方法返回一个嵌套成员数组。

这是一个通用示例的链接,由 Baeldung.com 提供,基于 Nest 的访问控制使好处非常突出,恕我直言。

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