Loading learning content...
We've established that runtime polymorphism allows the same method call to invoke different implementations based on the actual object type. But how does the runtime actually achieve this? When you call document.render() and document could be any of a dozen subtypes, what mechanism determines which render() method to execute?
The answer lies in a elegant piece of runtime engineering called Virtual Method Dispatch (also known as dynamic dispatch). Understanding this mechanism isn't just academic curiosity—it illuminates why certain language features exist, explains performance characteristics you'll encounter, and deepens your understanding of object-oriented design.
By the end of this page, you'll understand the virtual method table (vtable) mechanism, how objects carry type information at runtime, the step-by-step process of dynamic dispatch, and why 'virtual' methods exist in some languages but not others.
Let's frame the problem precisely. Consider this code:
Document doc = getDocumentFromSomewhere();
String output = doc.render();
At compile time, the compiler knows:
doc is of type Document (the declared type)Document has a method String render()doc.render() is therefore validBut the compiler does not know:
doc actually holds a Document, MarkdownDocument, RichTextDocument, or any other subtyperender() should be calledThe problem: The machine code for this method call must be generated at compile time, but the target method isn't known until runtime.
Possible Solutions:
There are a few ways a language could handle this:
Static dispatch only — Only call methods that can be determined at compile time. This eliminates runtime polymorphism entirely.
Type checking at every call — Store type information and use if/else chains to select methods. This is slow and doesn't scale.
Virtual method tables — Precompute method lookup tables, and use indirection to select methods efficiently at runtime.
Most object-oriented languages choose option 3, which provides the best balance of flexibility and performance.
In Java, all non-static, non-final, non-private methods are 'virtual' by default (they can be overridden and dynamically dispatched). In C++, you must explicitly mark methods as 'virtual'. In Python and other dynamic languages, all methods are dispatched dynamically. The underlying mechanism is similar across languages, even when terminology differs.
The most common implementation of dynamic dispatch uses a data structure called the Virtual Method Table (or vtable, sometimes called a VMT or dispatch table).
The Core Idea:
Every class that has virtual methods (methods that can be overridden) maintains a table of function pointers. Each entry in the table points to the actual implementation of a method for that class. When calling a virtual method:
Since every object carries a pointer to its class's vtable, and each subclass has its own vtable with its own method implementations, the correct method is always invoked.
How the Vtable Works:
vptr or similar) pointing to its class's vtableKey insight: The slot index for each method is the same across all classes in an inheritance hierarchy. If render() is at slot 1 in Document's vtable, it's at slot 1 in MarkdownDocument's vtable too. Only the pointer at that slot differs.
123456789101112131415161718192021222324252627282930313233343536373839
// Conceptually, this is what the compiler generates: // For each class with virtual methods, a vtable is createdstruct Document_VTable { void* (*toString)(Document* self); void* (*render)(Document* self); // Slot 1 void* (*getContent)(Document* self); // Slot 2}; struct MarkdownDocument_VTable { void* (*toString)(MarkdownDocument* self); void* (*render)(MarkdownDocument* self); // Slot 1 - DIFFERENT pointer void* (*getContent)(MarkdownDocument* self);}; // Static vtable instances for each classDocument_VTable document_vtable = { &Document_toString, &Document_render, // Points to Document's implementation &Document_getContent}; MarkdownDocument_VTable markdown_vtable = { &MarkdownDocument_toString, &MarkdownDocument_render, // Points to MarkdownDocument's implementation &MarkdownDocument_getContent}; // Each object has a hidden vptr fieldstruct Document { void** vptr; // Points to the vtable char* content; // ... other fields}; // When you call doc.render(), the compiler generates:// doc->vptr[1](doc); // Look up slot 1 in the vtable, call it// This is ONE pointer dereference + ONE indexed lookup// Same cost regardless of inheritance depth!Let's trace exactly what happens when a polymorphic method is called. We'll use Java for the example, but the concepts apply broadly:
1234
Document doc = new MarkdownDocument("Hello **world**");String result = doc.render(); // <-- This call // Let's trace what happens at runtime:new MarkdownDocument(...) allocates memory and sets the hidden vptr to point to MarkdownDocument's class metadata (which contains the vtable).doc of type Document now holds a pointer to this MarkdownDocument object.doc.render() is called, the JVM follows the object's vptr to find the class metadata.render() in the vtable. Since MarkdownDocument overrides render(), the entry points to MarkdownDocument's implementation.this) as an implicit argument.render() executes and returns HTML-converted content.Critically, the declared type (Document) was never consulted during dispatch. The runtime only used:
This is why a Document reference can seamlessly invoke MarkdownDocument behavior—the reference type only matters at compile time for type checking.
Compile-time (static): Verify the method exists, check parameter types, determine the vtable slot index. Runtime (dynamic): Follow the vptr, look up the slot, call the method. The actual method code is never known until this moment.
Virtual method dispatch isn't free—but it's remarkably efficient. Understanding the performance characteristics helps you make informed design decisions.
| Aspect | Static Dispatch | Virtual Dispatch | Impact |
|---|---|---|---|
| Method resolution | Direct function call | Vtable lookup + indirect call | ~2-5 nanoseconds typical overhead |
| Inlining potential | Can always inline | Cannot inline (usually) | May prevent important optimizations |
| Cache behavior | Predictable | May cause cache misses | Depends on access patterns |
| Branch prediction | No branches | Predictable indirect branch | Modern CPUs handle well |
| Memory overhead | None | vptr per object + vtable per class | 8 bytes/object typical |
When Does This Matter?
For the vast majority of code, virtual dispatch overhead is irrelevant. The flexibility benefits far outweigh a few nanoseconds per call. However, there are scenarios where it can accumulate:
Modern JIT compilers (like HotSpot in Java) perform devirtualization—if they can prove only one implementation exists or is used at a call site, they convert virtual calls to direct calls and even inline them. This eliminates most virtual dispatch overhead in hot paths.
Never avoid virtual methods for 'performance reasons' without measuring. Modern JVMs and CPUs are extremely good at optimizing virtual dispatch. In most applications, algorithm choice and I/O dominate performance—not dispatch overhead. Profile first, optimize later.
12345678910111213141516171819202122232425262728293031
// The JIT compiler can often eliminate virtual dispatch entirely public class OrderProcessor { private final PaymentHandler handler; // Type is interface public OrderProcessor(PaymentHandler handler) { this.handler = handler; } public void processOrders(List<Order> orders) { for (Order order : orders) { // This looks like a virtual call... handler.process(order); // But if the JIT observes that 'handler' is always // a CreditCardHandler at runtime (monomorphic call site), // it can: // 1. Devirtualize: treat as direct call // 2. Inline: embed the method body here // 3. Optimize: apply further optimizations // Result: zero virtual dispatch overhead in hot path! } }} // The JIT performs "speculative devirtualization":// - Checks if assumption holds (type guard)// - Uses optimized path if true// - Falls back to virtual dispatch if not// This is why Java code often performs better than expected!Not all methods use virtual dispatch. Understanding when methods are virtual helps you design classes intentionally.
In Java:
static methods are resolved at compile time (not virtual)final methods cannot be overridden, so JIT often devirtualizes themprivate methods cannot be overridden (not visible to subclasses)In C++:
virtualoverride keyword helps catch mistakesIn Python:
_ prefix) can be overridden1234567891011121314151617181920212223242526272829
public class Parent { // Virtual - can be overridden public void virtualMethod() { } // Virtual - can be overridden protected void alsoVirtual() { } // NOT in vtable - subclasses // can't see it private void notVirtual() { } // NOT virtual - can't override public final void finalMethod() { } // NOT virtual - belongs to class public static void staticMethod() { }} public class Child extends Parent { @Override public void virtualMethod() { // Successfully overrides } // Cannot override: // - private (not visible) // - final (explicitly sealed) // - static (wrong concept)}12345678910111213141516171819202122232425262728293031323334
class Parent {public: // NOT virtual by default! void nonVirtualMethod() { } // Explicitly virtual virtual void virtualMethod() { } // Virtual and pure abstract virtual void pureVirtual() = 0; // Prevent override in children virtual void sealedMethod() final { } protected: virtual void protectedVirtual() { }}; class Child : public Parent {public: // 'override' catches mistakes: // - Wrong signature = error // - Non-virtual parent = error void virtualMethod() override { // Successfully overrides } void pureVirtual() override { // Must implement } // ERROR: sealedMethod is final // void sealedMethod() override { }};In Java, consider marking methods final when they shouldn't be overridden—not for performance, but for correctness. This prevents accidental behavioral changes that could break invariants. In C++, prefer virtual only where polymorphism is intended, following 'you don't pay for what you don't use.'
While the concept of dynamic dispatch is universal, implementation details vary:
Java / C#:
C++:
Python / Ruby / JavaScript:
| Language | Default Virtual | Mechanism | Optimization |
|---|---|---|---|
| Java | Yes (instances) | Vtable + JIT | Devirtualization, inlining |
| C++ | No (explicit) | Vtable | LTO, devirtualization |
| C# | No (virtual keyword) | Vtable + JIT | Similar to Java |
| Python | Yes (always) | Dict lookup | Inline caches |
| JavaScript | Yes (always) | Hidden classes | JIT inline caches |
| Go | Interface only | Interface tables | Escape analysis |
The Trade-off:
Languages with explicit virtual (C++) give programmers control over when dynamic dispatch is used, avoiding overhead where not needed. Languages with implicit virtual (Java) provide more flexibility by default, relying on the optimizer to eliminate unnecessary dispatch.
Neither approach is inherently better—they represent different design philosophies and trade-offs between control and convenience.
We've explored the machinery that makes runtime polymorphism possible. Let's consolidate:
What's Next:
Now that we understand both why overriding enables polymorphism and how dynamic dispatch works, we'll turn to practical guidance. In the next page, we'll explore Override Annotations—language features that help us declare intent, catch errors, and make our code more robust.
You now understand the virtual method table mechanism that powers runtime polymorphism. Each object carries a pointer to its class's method table, enabling efficient method lookup at runtime. This is the engineering foundation that makes flexible, substitutable object-oriented design possible at practical performance levels.