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.
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 classClass
and not inConsole
.
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() {}
}
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() {}
}
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
}