javajvmjvm-bytecodeinvokevirtual

Java Bytecode: invokevirtual methodref on an objectref with a different class


I am currently looking into how Java bytecode works. I created this simple test class:

class Main {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

With javap -c Main.class I can get its bytecode:

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

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #13                 // String Hello, World!
       5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

The first instruction that should be executed (according to my understanding) should be the getstatic instruction in the main function. This leads to the System class and others like Console being loaded.

During the <clinit> method of Console:

static {};
    Code:
       0: ldc           #2                  // class java/io/Console
       2: invokevirtual #203                // Method java/lang/Class.desiredAssertionStatus:()Z
...

a invokevirtual method is executed to call the desiredAssertionStatus function on the class Class. One can already see a difference between this invokevirtual instruction and the one above: javap appends the class name before the method name because the function is found in the class Class and not in Console.

So finally my question is: What is going on here? According to the JVM 19 specification for invokevirtual the method should be invoked on the object that was on the stack. In this case Console but this class doesn't have the requested method nor is it a subclass of Class. What does the JVM intend to do here/how do JVM implementations like Hotspot go about doing this. Do they just put the method onto the Console class for the duration of the instruction or somehow inject the class Class into Console or maybe there is something completely different going on here that I am missing?

Anyways thank you for taking your time to read my question!

I hope you have/had a wonderful day :)

I thought about doing the following things:


Solution

  • You wrote

    One can already see a difference between this invokevirtual instruction and the one above: javap appends the class name before the method name because the function is found in the class Class and not in Console.

    but there is no difference in that regard. javap included the class name in both cases. In case of the println method, it’s the class java/io/PrintStream.

    For ordinary Java code, the ldc instruction may load primitive values or objects of type String or Class. We can reproduce such cases with

    public class Main {
      static {
            Console.class.desiredAssertionStatus();
            "hello".toString();
        }
      
        public static void main(String[] args) {
            showBytecode();
        }
      
        private static void showBytecode() {
            ToolProvider.findFirst("javap")
                .ifPresent(p -> p.run(System.out, System.err, "-c", "Main"));
        }
      
        private Main() {}
    }
    
    online example
    Compiled from "Main.java"
    public class Main {
      public static void main(java.lang.String[]);
        Code:
           0: invokestatic  #1                  // Method showBytecode:()V
           3: return
    
      static {};
        Code:
           0: ldc           #13                 // class java/io/Console
           2: invokevirtual #14                 // Method java/lang/Class.desiredAssertionStatus:()Z
           5: pop
           6: ldc           #15                 // String hello
           8: invokevirtual #16                 // Method java/lang/String.toString:()Ljava/lang/String;
          11: pop
          12: return
    }
    

    We can see that javap will always print the actual value after an ldc instruction, so in this example, “class java/io/Console” and “String hello”.

    The ldc instruction can also load objects of other types, but there are no ordinary Java language equivalents to these use cases.

    The desiredAssertionStatus() query is, by the way, typically used to implement the assert statement support, eg.

    public class Main {
        public static void main(String[] args) {
            assert "foo".length() == 3;
            showBytecode();
        }
    
        private static void showBytecode() {
            ToolProvider.findFirst("javap")
                .ifPresent(p -> p.run(System.out, System.err, "-c", "Main"));
        }
    
        private Main() {}
    }
    
    gets compiled to
    Compiled from "Main.java"
    public class Main {
      static final boolean $assertionsDisabled;
    
      public static void main(java.lang.String[]);
        Code:
           0: getstatic     #1                  // Field $assertionsDisabled:Z
           3: ifne          23
           6: ldc           #2                  // String foo
           8: invokevirtual #3                  // Method java/lang/String.length:()I
          11: iconst_3
          12: if_icmpeq     23
          15: new           #4                  // class java/lang/AssertionError
          18: dup
          19: invokespecial #5                  // Method java/lang/AssertionError."<init>":()V
          22: athrow
          23: invokestatic  #6                  // Method showBytecode:()V
          26: return
    
      static {};
        Code:
           0: ldc           #18                 // class Main
           2: invokevirtual #19                 // Method java/lang/Class.desiredAssertionStatus:()Z
           5: ifne          12
           8: iconst_1
           9: goto          13
          12: iconst_0
          13: putstatic     #1                  // Field $assertionsDisabled:Z
          16: return
    }