In our last lesson, we examined the precise memory layout of a Java object in the HotSpot JVM, highlighting the crucial role of the object header and its two main components: the mark word and the klass pointer. You learned that this klass pointer is a reference to an object's class metadata stored in Metaspace. Today, we will see exactly how the JVM uses this pointer to execute one of object-oriented programming's most fundamental features: polymorphism.
This lesson unpacks the mechanics of method calls in Java. Our learning outcome is to explain the difference between static and dynamic method dispatch and understand how the JVM resolves virtual calls using virtual tables (vtables) and interface calls using interface tables (itables). We will go from the high-level concept of polymorphism down to the data structures and optimizations that make it efficient at runtime.

Static vs. Dynamic Dispatch: A Tale of Two Bindings
When you call a method in Java, the JVM must determine exactly which block of code to execute. This decision-making process, called method dispatch (or method binding), can happen at two different times: compile time or runtime.
Static Dispatch (Early Binding)
This occurs when the compiler can determine the exact method to be called without any ambiguity. Since the decision is made early (at compile time), it's called early binding. This is the case for methods that cannot be overridden:
privatemethods: They are only visible within their own class, so there's no possibility of a subclass overriding them.staticmethods: They belong to the class, not an object instance, so their implementation is fixed.finalmethods: Thefinalkeyword explicitly prevents overriding.- Constructors: These are implicitly private and are called with a special bytecode instruction,
invokespecial.
In all these cases, the compiler knows the exact memory address of the code to execute and can hardcode it into the bytecode.
Dynamic Method Dispatch (Late Binding)
This is the mechanism that enables polymorphism. It occurs when the method to be called depends on the actual runtime type of the object, not the compile-time type of the reference variable. The decision is deferred until runtime, hence "late binding."
Let's see this in action. The following video provides a clear, code-driven example of runtime polymorphism.
#56 Dynamic Method Dispatch in Java
This video from Telusko demonstrates the core concept of dynamic method dispatch.
Watch the following segments to see how Java handles this: First, understand the premise of a superclass reference holding a subclass object. Next, see what happens when the subclass overrides a method. Notice which version of the method gets called. Finally, observe how the same method call behaves differently when the reference points to different subclass objects. This is the essence of dynamic dispatch.
As the video demonstrates, when you have A obj = new B();, a call to obj.show() executes the show() method from class B, even though the reference obj is of type A. The compiler only knows that obj has a show() method, but the JVM, at runtime, inspects the actual object (new B()) and dispatches the call to its specific implementation.
But how does the JVM perform this "inspection" so efficiently?
The Vtable: A Class's Method Directory
The secret lies in the virtual table, or vtable. In the last lesson, you learned that every object has a klass pointer in its header. This pointer directs the JVM to a C++ structure in Metaspace called InstanceKlass, which represents the object's class. As you'll see in the resources, this InstanceKlass contains the vtable.
A vtable is an array of pointers to method implementations. For a given class hierarchy:
- The base class (
Animal) has a vtable with entries for all its virtual methods (in Java, all non-private, non-static, non-final methods are virtual by default). - When a subclass (
Dog) extends the base class, it gets a copy of the parent's vtable. - If the subclass overrides a method (e.g.,
speak()), the corresponding pointer in its vtable is updated to point to theDog.speak()implementation. - If it doesn't override a method (e.g.,
eat()), that vtable entry continues to point to theAnimal.eat()implementation.
A virtual method call, which uses the invokevirtual bytecode instruction, is resolved as a three-step process:
- Get the object's
klasspointer from its header. - From the
InstanceKlass, find the vtable. - Look up the method pointer at a pre-determined, fixed offset in the vtable and jump to that code address.
This concept of vtables is not unique to Java. Given your background in C, the following explanation, which approaches the topic from an assembly and C++ perspective, will provide a solid, low-level intuition.
Object-Oriented Programming in Assembly Explained | Vtables, Polymorphism | Python, Java, C++, C#
The channel Mxy offers a superb explanation of how object-oriented concepts are realized at the machine level.
Watch these key parts to understand the mechanics of vtables: The introduction to virtual methods and vtables explains why a "secret pointer" is added to objects and how the vtable is structured. The demonstration of the vtable in action shows how it's used to resolve a call when the compiler isn't sure of the object's concrete type. Finally, the comparison of dispatch defaults in C++, C#, and Java explicitly confirms that Java methods are dynamically dispatched by default.
The Interface Challenge: Why Vtables Aren't Enough
The vtable model works beautifully for single class inheritance because there is a clear, linear hierarchy. But what about interfaces? A class can implement multiple interfaces, each with its own set of methods.
Consider this scenario:
class Car implements Drivable, Maintainable { ... }
Drivable d = new Car();
d.turn("left"); // Method from Drivable
Maintainable m = new Car();
m.changeOil(); // Method from Maintainable
The Drivable interface might define turn() as its first method, so its vtable slot would be at index 0. The Maintainable interface might define changeOil() as its first method, also at index 0. A single vtable in the Car class cannot satisfy both layouts simultaneously.
To solve this, the JVM uses a different mechanism for interface method calls (invokeinterface): interface tables, or itables.
Inside the JVM: The Engineering Behind Enterprise Performance
The technical article from TM Dev Lab confirms where vtables and itables reside.
Find the section "3. The Object Model: oops and Klass" and read the paragraph on the Klass Hierarchy. It explicitly states that InstanceKlass contains both the vtable and the itable.
An InstanceKlass for a class contains a separate itable for each interface it implements. An itable is a data structure that links an interface to a method table specific to that interface's layout. When an interface method is called, the JVM performs a more complex lookup:
- Follow the object's
klasspointer to itsInstanceKlass. - Search through the list of itables in the
InstanceKlass. - For each itable, check if it corresponds to the target interface.
- Once the correct itable is found, use the method's fixed offset within that table to find the method address and execute it.
This process, especially the linear search through itables, sounds much slower than a direct vtable lookup. And it is. But the JVM has a powerful trick up its sleeve.
Interface Dispatch | Lukas Atkinson
Lukas Atkinson's blog post provides an excellent deep dive into this topic, comparing Java's approach to other languages.
Read the section on Java's itables. The pseudocode for resolve_method clearly illustrates the search process we just described.
Making Dynamic Dispatch Fast: Inline Caching
In practice, dynamic dispatch is often just as fast as static dispatch. This is thanks to aggressive optimization by the Just-In-Time (JIT) compiler, primarily through a technique called inline caching.
The JIT observes code as it runs. At a virtual or interface call site (e.g., animal.speak()), it notices that the object is usually of the same type (e.g., Dog).
- Monomorphic Call Site: If a call site has only ever seen one type of object (
Dog), the JIT can perform a radical optimization. It replaces the expensive vtable/itable lookup with a simple check:if (animal.getClass() == Dog.class). If this check passes, it makes a direct, hardcoded call toDog.speak(), effectively bypassing dynamic dispatch entirely. This is the "fast path." - Polymorphic/Megamorphic Call Site: If the call site sees a few different types (bimorphic) or many types (megamorphic), the JIT may use slightly more complex checks or eventually give up and revert to the full, slower vtable/itable lookup.
This adaptive optimization is why performance-critical code in Java can heavily leverage polymorphism without a significant penalty.
Interface Dispatch | Lukas Atkinson
The same article explains this optimization beautifully.
Read the section on Inline caching. The pseudocode again illustrates how a call site can be "patched" with a cached type and method to create a fast path.
Inside the JVM: The Engineering Behind Enterprise Performance
The TM Dev Lab article details how HotSpot implements this.
Under the "Execution Engine" section, read about call site specialization to understand the terminology of monomorphic, bimorphic, and megamorphic call sites. Then, review the code examples which demonstrate the performance implications.
Conclusion
Today, we've connected the dots from the high-level concept of polymorphism to the low-level JVM machinery that makes it work. You now have a robust mental model of how your method calls are executed "under the hood."
Key Takeaways:
- Static Dispatch is for methods whose implementation is known at compile time (
private,static,final), making it fast and simple. - Dynamic Dispatch is for virtual methods, where the implementation is chosen at runtime based on the object's actual type, enabling polymorphism.
- The JVM uses vtables for virtual method calls within a class hierarchy. This is a direct, offset-based lookup that is very efficient.
- For interface methods, the JVM uses itables, which involves a slower search but correctly handles multiple interface inheritance.
- The
klasspointer in an object's header is the starting point for both vtable and itable lookups. - Inline Caching by the JIT compiler is a critical optimization that makes dynamic dispatch extremely fast in most real-world scenarios by creating "fast paths" for frequently seen types at call sites.
In our next module, we will shift our focus from how objects behave to how they are managed and eventually reclaimed. Now that you understand an object's life and function, we will explore its death and recycling in our first lesson on Garbage Collection fundamentals.