When an instance of a derived class is created the heap allocation will be something like (*):
- Standard JVM object header (with pointer to Class object for
DerivedClass
)
- Instance fields for class
Object
- Instance fields for class
BaseClass
- Instance fields for class
DerivedClass
So in effect, if you ignore the DerivedClass
instance fields the object looks remarkably like an instance of BaseClass, and the JVM can reference the object as if it were an instance of BaseClass and have no difficulty doing so.
Similarly, in the Class object for DerivedClass
is a "virtual method table" with:
- Virtual method pointers for
Object
- Virtual method pointers for
BaseClass
- Virtual method pointers for
DerivedClass
The JVM makes virtual calls by indexing into this table to find a specific method, knowing that, say, hashValue
is method number 5 and printTheGroceryList
is method number 23. The number needed to call a method is determined when a class is loaded and cached in the method reference data in calling classes, so calling a method is: Get the number, go the the Class object pointed to by the instance header, index into the virtual method table, pull out the pointer, and branch to the method.
But when you look closely you will see, eg, that the pointer in the Object
group that points to the hashValue
method actually points to the one in BaseClass
(if BaseClass overrides hashValue
). So the JVM can treat the object as if it were an Object
, invoke hashValue
, and seamlessly get the method in BaseClass
(or DerivedClass
, if it also overrides the method).
(*) In practice the instance fields may be intermingled to a degree, since "aligning" fields from a superclass may leave gaps in the heap allocation that fields from a subclass can fill. This is simply a trick to minimize object size.