vtable & Dynamic Dispatch
When you call an overridden method on a parent-type reference, Java somehow knows which subclass version to run — every single time, without error. The machinery behind that magic is called the virtual method table (vtable) and the dynamic dispatch mechanism built into the JVM.
What Is a vtable?
A vtable (virtual method table) is a data structure the JVM creates for each class that holds a list of pointers to the actual method implementations the class provides. You never write one yourself — the JVM builds it automatically when it loads a class.
Think of it as a lookup table: each row corresponds to a virtual (overridable) method, and each cell points to the concrete code that should run. When a subclass overrides a method, its vtable row for that method is updated to point to the new implementation. If a subclass does not override the method, it simply inherits the pointer from the parent’s table.
| vtable slot | Animal’s vtable | Dog’s vtable |
|---|---|---|
| slot 0 | Animal.sound() code | Dog.sound() code (overridden) |
| slot 1 | Animal.eat() code | Animal.eat() code (inherited) |
Note: Only instance methods that can be overridden get vtable entries.
staticmethods,privatemethods, andfinalmethods are resolved at compile time — they do NOT participate in dynamic dispatch.
Dynamic Dispatch Step by Step
When you write animal.sound() where animal is declared as Animal but actually holds a Dog at runtime, here is exactly what happens:
- The JVM looks at the object reference stored in
animal. - It follows the reference to the actual
Dogobject on the heap. - Every object on the heap contains a hidden pointer to its class’s vtable (called the klass pointer or class word).
- The JVM looks up the
sound()slot inDog’s vtable. - It calls whatever method pointer lives there —
Dog.sound().
This whole lookup is extremely fast; it is essentially a couple of pointer dereferences.
Seeing Dynamic Dispatch in Action
class Animal {
void sound() {
System.out.println("Generic animal sound");
}
void breathe() {
System.out.println("Breathing...");
}
}
class Dog extends Animal {
@Override
void sound() { // overrides — new vtable entry
System.out.println("Woof!");
}
// breathe() not overridden — inherits Animal's vtable pointer
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Meow!");
}
}
public class VTableDemo {
public static void main(String[] args) {
Animal a1 = new Dog(); // declared Animal, actual Dog
Animal a2 = new Cat(); // declared Animal, actual Cat
a1.sound(); // JVM checks Dog's vtable → Dog.sound()
a2.sound(); // JVM checks Cat's vtable → Cat.sound()
a1.breathe(); // Dog has no override → Animal.breathe()
}
}
Output:
Woof!
Meow!
Breathing...
The variable type (Animal) is irrelevant at runtime. The vtable lookup on the actual object decides everything.
The Bytecode: invokevirtual
The compiler translates a virtual method call into the invokevirtual bytecode instruction. You can verify this with the javap tool (see javap Tool):
// Compile VTableDemo.java, then run:
// javap -c VTableDemo
Inside main, you will see lines like:
invokevirtual #7 // Method Animal.sound:()V
Notice the instruction says Animal.sound — that is the declared type. At runtime the JVM replaces that with the correct subclass version through the vtable. The compiler only ever sees the declared type; the JVM does the actual dispatch.
Tip: Compare this with
invokespecial, which the JVM uses forprivatemethods, constructors, andsupercalls. Those are resolved statically — no vtable lookup.
What Does NOT Use the vtable
Not every method goes through dynamic dispatch. These are resolved at compile or link time:
staticmethods — useinvokestatic; belong to the class, not an instance.privatemethods — useinvokespecial; cannot be overridden.finalmethods — the JIT compiler often inlines them completely.- Constructors — use
invokespecial. - Interface default methods — use
invokeinterface, a separate (slightly more complex) dispatch mechanism.
This is also why calling a method through a static or private reference is slightly faster in theory — though in practice the JIT compiler optimizes virtual calls so aggressively that the difference rarely matters.
How Interfaces Differ: itable
When you call a method through an interface reference, the JVM uses a companion structure called the itable (interface method table). The itable is separate because a class can implement many interfaces, so a single flat vtable would conflict. The lookup is slightly more work, but the JIT still manages to inline the most common case.
interface Speakable {
void speak();
}
class Parrot implements Speakable {
@Override
public void speak() {
System.out.println("Polly wants a cracker!");
}
}
public class ItableDemo {
public static void main(String[] args) {
Speakable s = new Parrot(); // interface reference
s.speak(); // uses invokeinterface → itable lookup
}
}
Output:
Polly wants a cracker!
Under the Hood
Object Header and klass Pointer
Every Java object on the heap begins with a small object header (typically 12–16 bytes on a 64-bit JVM with compressed oops). Part of that header is the klass pointer — a reference to the HotSpot internal Klass structure for the object’s actual class. That Klass structure contains the vtable. So the dispatch path is:
object reference → object header → klass pointer → Klass struct → vtable → method code
All of this happens in nanoseconds.
JIT Inlining and Devirtualization
The HotSpot JIT compiler is very smart about virtual dispatch. When profiling data shows that a vtable slot almost always resolves to the same concrete class, the JIT performs devirtualization: it replaces the vtable lookup with a direct call (or even inlines the method body entirely), then adds a guard check to fall back to the vtable if a different class appears. This means hot virtual calls often cost the same as direct calls.
This is one reason you should not prematurely optimize by making methods final just to “help the JIT” — the JIT already devirtualizes based on real profiling.
Sealed Classes and vtable (Java 17+)
Sealed classes (stable since Java 17) explicitly restrict which classes can extend a given type. This gives the JIT even stronger guarantees for devirtualization, because it can statically prove that only a small, known set of subclasses will ever appear.
Practical Implications for You
Understanding vtables helps you reason about several everyday decisions:
- Why
@Overridematters: if you accidentally misspell a method name, the compiler catches it because no vtable slot exists for your “new” method. Always annotate with@Override. - Why polymorphism is safe: the vtable mechanism means the JVM cannot call the wrong method — the correct pointer is always in the table.
- Why
staticmethods are not polymorphic: they bypass the vtable entirely. Declaring astaticmethod with the same signature in a subclass is called method hiding, not overriding, and it does not participate in dynamic dispatch (see static Keyword). - Performance: virtual dispatch is effectively free in modern JVMs for hot code paths thanks to JIT devirtualization. Design for correctness and clarity, not to avoid vtable lookups.
Warning: Do not call overridable (virtual) methods from a constructor. At the time the parent constructor runs, the subclass object is only partially initialized, but dynamic dispatch will still route to the subclass override — potentially reading uninitialized fields.
class Base {
Base() {
init(); // dangerous — dispatches to Child.init() before Child fields are set
}
void init() { System.out.println("Base.init"); }
}
class Child extends Base {
int value = 42;
@Override
void init() {
System.out.println("Child.init, value = " + value); // prints 0, not 42!
}
}
Output:
Child.init, value = 0
The field value is still 0 because the Child constructor body (where value = 42 runs) has not executed yet.
Related Topics
- Runtime Polymorphism — the high-level concept that vtable dispatch implements
- Method Overriding — how subclasses supply the new vtable entries
- Static & Dynamic Binding — compile-time vs runtime resolution explained side by side
- JIT Compilation & Bytecode — how the JIT turns
invokevirtualinto fast native code - Abstract Class — enforces overriding, so every vtable slot is filled by a concrete subclass
- Interfaces — the itable counterpart to vtables for interface method dispatch