I think I have finally found out how to word, what is giving me so much trouble in understanding: how the virtual machine can access a classes methods and use it only on a given instance (object) with the catch that the virtual machine is only being given the reference/pointer variable.
This was compounded by the fact that most visualizations of the methods interacting with the stack/heap (that is shown to most beginner Java programmers) don’t quite go deep enough into the depth I am in looking for.
I have done a lot of research, and I want to say a good summary of what I learned, and I am asking if you could please correct me where I am wrong (or elaborate further if you think there is more that could be said)! Note that I am using this portion of an article I found (I am using it more as a visual reference, I understand some of the text in the article does not pertain to the question), so please take a look at it before reading onward:
So let’s say I have a reference/pointer variable foo1
that is of type Foo
(was created using a constructor called Foo
). foo1
is stored on the stack, but the object it points to is stored on the heap (the Foo
object having an instance variable int size;
).
So I understand how foo1.size
would give the integer value of size
because the value of foo1
is dereferenced to get the value field of size
(the reference/pointer variable has a direct address where the size
field is stored on the heap in the object).
But when foo1.bar()
is ran, what exactly does its bytecode translate to? And how is this method call performed at runtime (would it be correct to say the value of foo1
is being dereferenced to get method bar()
)?
Does it relate correctly to the diagram in the image above (all in the JVM: does it go from the reference/pointer variable foo1
on the stack to the heap which is actually a pointer to another pointer (which points to the bytecode of all the class data) full class data
(in a method table
which is just an array of pointers to the data for each instance method that can be invoked on objects of that class) in the method area which then itself has “pointer variables” to the actual bytecode method data
)?
I apologize for how long-winded this post is, but I want to be extremely specific since I have had major trouble the past week trying to word my question properly. I know I sound sceptical of the article I am referencing, but it seems there is a lot of junk visualizations out there and I want to be sure that I’m continuing my Java programming correctly, and not on incorrect notions.
Advertisement
Answer
Ordinary instance method invocations get compiled to invokevirtual
instructions.
This has been described in JVMS, §3.7. Invoking Methods:
The normal method invocation for a instance method dispatches on the run-time type of the object. (They are virtual, in C++ terms.) Such an invocation is implemented using the invokevirtual instruction, which takes as its argument an index to a run-time constant pool entry giving the internal form of the binary name of the class type of the object, the name of the method to invoke, and that method’s descriptor (§4.3.3). To invoke the
addTwo
method, defined earlier as an instance method, we might write:int add12and13() { return addTwo(12, 13); }This compiles to:
Method int add12and13() 0 aload_0 // Push local variable 0 (this) 1 bipush 12 // Push int constant 12 3 bipush 13 // Push int constant 13 5 invokevirtual #4 // Method Example.addtwo(II)I 8 ireturn // Return int on top of operand stack; // it is the int result of addTwo()The invocation is set up by first pushing a
reference
to the current instance,this
, on to the operand stack. The method invocation’s arguments,int
values12
and13
, are then pushed. When the frame for theaddTwo
method is created, the arguments passed to the method become the initial values of the new frame’s local variables. That is, thereference
forthis
and the two arguments, pushed onto the operand stack by the invoker, will become the initial values of local variables0
,1
, and2
of the invoked method.
It’s up to the particular JVM implementation, how to perform the invocation at runtime, but using a vtable is very common. This basically matches the graphic in your question. The reference to the receiver object, which will become the this
reference for the invoked method, is used to retrieve a method table.
In the HotSpot JVM, the metadata structure is called Klass
(actually a common name, even across different implementations). See “Object header layout” on the OpenJDK Wiki:
An object header consists of a native-sized mark word, a klass word, a 32-bit length word (if the object is an array), a 32-bit gap (if required by alignment rules), and then zero or more instance fields, array elements, or metadata fields. (Interesting Trivia: Klass metaobjects contain a C++ vtable immediately after the klass word.)
When resolving a symbolic reference to a method, its corresponding index in the table will be identified and remembered for subsequent invocations, as it never changes. Then, the entry of the actual object’s class can be used for the invocation. Subclasses will have the entries of the superclass, new methods appended to the end, with the entries of overridden methods replaced.
This is the simple, unoptimized scenario. Most runtime optimizations work better when methods are inlined, to have the context of caller and callee in one piece of code to transform. Therefore, the HotSpot JVM will attempt inlining even for invokevirtual instructions to potentially overridable methods. As the wiki says:
- Virtual (and interface) invocations are often demoted to “special” invocations, if the class hierarchy permits it. A dependency is registered in case further class loading spoils things.
- Virtual (and interface) invocations with a lopsided type profile are compiled with an optimistic check in favor of the historically common type (or two types).
- Depending on the profile, a failure of the optimistic check will either deoptimize or run through a (slow) vtable/itable call.
- On the fast path of an optimistically typed call, inlining is common. The best case is a de facto monomorphic call which is inlined. Such calls, if back-to-back, will perform the receiver type check only once.
This aggressive or optimistic inlining will sometime require Deoptimization but will usually yield an overall higher performance.