2

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:

  • Adding the method to the Console class for the duration of the instruction. This doesn't work because the method requires fields from the class Class.

  • Actually just calling the method on an instance of Class which is created on the fly but I think these seems weird.

  • This might be a very special case because I think that it has something to do with Console trying to interact with its ClassLoader. If this is a special case and there aren't many occurrences of this happening: Maybe the JVM just does some under the hood magic like assigning each class an instance of Class after loading which is then used for just this. Also seems weird to me.

  • `ldc` **l**oa**d**s a **c**onstant. – Johannes Kuhn Dec 15 '22 at 20:22
  • 1
    Adding more to first comment: What do you think that ldc #2 instruction does? I'll give you a hint: It's not pushing an instance of Console to the stack. – boneill Dec 16 '22 at 01:35
  • [ldc spec](https://docs.oracle.com/javase/specs/jvms/se19/html/jvms-6.html#jvms-6.5.ldc) so its actually pushing a reference to `Class` onto the stack? If that is the case then I am very sorry. Typical case of not seeing the forest because of all the trees. I assumed it would push a reference to the class with the name at that index onto the stack but reading your comments I see now that it might be referring to the class `Class`? – Tim Engbrocks Dec 16 '22 at 07:26

1 Answers1

3

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
}
Holger
  • 285,553
  • 42
  • 434
  • 765