skip to content

How does the JVM resolve an overridden method call at runtime? Explain dynamic method dispatch.

level: middleimportance: must knowfreq 74%

answer

  1. Object header → class → method table (vtable) → slot → jump
  2. Compiler emits invokevirtual; runtime resolves it
  3. Receiver's REAL class wins, not the declared type
  4. Statics, privates, finals, fields are NOT dispatched
  5. JIT inlines monomorphic sites with a guard

basics

~20 s

Each object knows its real class. When you call an overridden method through a parent-type reference, the JVM looks up the method on the object's actual class at runtime and runs that version. This runtime lookup is called dynamic method dispatch.

solid answer

~50 s

Dynamic method dispatch is the runtime mechanism that decides which overridden method body to execute. The compiler cannot know the real object behind a reference, so for a virtual (overridable, instance, non-private, non-static, non-final) call it emits an invokevirtual (or invokeinterface) bytecode instruction instead of binding a fixed method. At runtime each object header points to its class, and each class has a method table (a vtable-like structure) mapping each overridable method slot to the most specific implementation for that class. invokevirtual reads the receiver object's class, indexes that table, and jumps to the resolved method. So `Animal a = new Dog(); a.sound();` runs Dog.sound() because the object's class is Dog. This is also called late binding. The JIT often optimizes monomorphic call sites by inlining and guarding, falling back to the table lookup only when the type varies.

code

java · 12 lines
java
class Animal { String sound() { return "..."; } }
class Dog extends Animal {
    @Override String sound() { return "Woof"; }
    String describe() { return "A " + super.sound() + "-default animal that says " + sound(); }
    // super.sound() -> invokespecial (Animal.sound, bypasses dispatch)
    // sound()       -> invokevirtual (Dog.sound,   dynamic dispatch)
}

Animal a = new Dog();
a.sound();   // invokevirtual: receiver's class is Dog -> "Woof"
// declared type Animal only decides that sound() is callable;
// the runtime object decides WHICH sound() body executes.

go deeper

for a junior

Knows that the object 'remembers' its real class and Java calls the overridden version at runtime; can state that this is dynamic dispatch without the bytecode detail.

for a middle

Explains the mechanism: receiver's runtime class plus a method table, invokevirtual, declared type only gates visibility. Knows statics/fields are not dispatched.

for a senior

Adds the four invoke bytecodes, vtable slot O(1) lookup, invokeinterface nuances, super/invokespecial, and field/static hiding; can reason about correctness edge cases.

for a principal

Discusses JIT optimization (monomorphic inlining, inline caches, megamorphic deopt), performance trade-offs of deep hierarchies vs interfaces, and design implications for hot paths and API extensibility.

## The problem dynamic dispatch solves Consider: ```java Animal a = pickRandomAnimal(); // could return a Dog, Cat, or Cow a.sound(); ``` The **compiler** sees only that `a` is declared as `Animal` (its *static type*). It literally cannot know which concrete object will exist when the line runs. Yet each subclass has its own `sound()`. Something must choose the right one **at runtime, based on the real object** — that something is **dynamic method dispatch** (also called *dynamic dispatch*, *virtual dispatch*, or *late binding*). ## Key term: virtual method A **virtual method** is one whose actual implementation is chosen at runtime by the receiver's type. In Java, **every non-`private`, non-`static`, non-`final` instance method is virtual by default** (unlike C++ where you opt in with the `virtual` keyword). `private`, `static`, and `final` methods, plus constructors, are *not* virtual — they bind statically. ## What the compiler emits: the four invoke bytecodes When Java is compiled to bytecode, a method call becomes one of these instructions: - **`invokevirtual`** — normal virtual instance call; resolved by the object's class at runtime (the heart of dynamic dispatch). - **`invokeinterface`** — like `invokevirtual` but the static type is an interface. - **`invokespecial`** — for constructors, `private` methods, and `super.x()` calls; bound to an *exact* method, NOT dispatched dynamically. - **`invokestatic`** — for `static` methods; no object, no dispatch. So the distinction between static and dynamic binding is visible right in the bytecode the compiler produces. ## The runtime mechanism: object header + method table Every Java object stored on the heap carries a hidden **object header** that, among other things, points to a **class metadata structure** describing its actual class. That class structure contains a **method table** — conceptually a *virtual method table* or **vtable**, an array where each overridable method occupies a fixed *slot*. Crucially, a subclass's table keeps the **same slot index** for an inherited method but points that slot at the **most specific implementation** for that class. So, for `a.sound()` compiled to `invokevirtual`: 1. The JVM takes the **receiver** (the object `a` refers to). 2. Follows the object header to the object's **actual class** (e.g. `Dog`). 3. Indexes the fixed **slot** for `sound()` in that class's method table. 4. Jumps to whatever implementation that slot holds — `Dog.sound()`. Because the slot index is constant across the hierarchy, this is roughly **O(1)** — a couple of pointer dereferences and an indexed jump, not a search. For `invokeinterface` the lookup is slightly more involved (interface method tables are not perfectly aligned across unrelated classes), but conceptually the same. ## Why the declared type does not matter for the *implementation* The declared type (`Animal`) only governs **what the compiler will let you call** — you can only invoke methods visible on `Animal`. But *which body runs* is decided entirely by the runtime object. Hence: ```java Animal a = new Dog(); a.sound(); // Dog.sound() — runtime object wins ``` ## Contrast: things that are NOT dynamically dispatched - **Static methods**: `Parent.foo()` vs `Child.foo()` are *hidden*, not overridden. `Parent p = new Child(); p.foo();` calls **Parent.foo()** — resolved by static type via `invokestatic`. - **Fields**: `Parent p = new Child(); p.x` reads **Parent's** `x` — field access uses the static type (field hiding). - **`private` methods**: not visible to subclasses, bound with `invokespecial`. - **Constructors**: never virtual. ## The `super` exception `super.sound()` deliberately bypasses dynamic dispatch (via `invokespecial`) to call the *parent's* implementation directly even though the runtime object is the subclass. ## Performance: the JIT's role A naive table lookup is cheap but not free, and it blocks inlining. The HotSpot **JIT** profiles call sites: - A **monomorphic** site (only ever sees one type) is inlined with a cheap type guard; if the guard holds, there is effectively zero dispatch cost. - **Bimorphic/polymorphic** sites use inline caches; megamorphic sites fall back to the vtable lookup. This is why "virtual calls are slow" is largely a myth on the JVM in practice. ## Deriving the answer The whole thing reduces to one sentence: **the object carries its class; a virtual call (`invokevirtual`) looks up the method on that class's table at runtime and jumps there.** Everything else — what's virtual, what's hidden, why `super` differs, why it's fast — follows from that mechanism.

  • Does dynamic dispatch apply to static methods?
    No. Static methods are not virtual; they are *hidden*, not overridden. A call through a parent reference uses the parent's static method (invokestatic / static-type resolution). Only instance, non-private, non-final methods are dispatched dynamically.
  • How does super.method() interact with dynamic dispatch?
    It bypasses it. super.method() compiles to invokespecial and calls the immediate parent's implementation directly, even though the runtime object is the subclass — useful when an override wants to extend rather than replace the parent behavior.

saying these in an interview costs you the question

  • Saying the declared/reference type chooses the implementation
  • Claiming static methods are dynamically dispatched (they are hidden)
  • Thinking field access is polymorphic
  • Believing virtual dispatch requires a linear search of the hierarchy
  • Assuming virtual calls are always a major performance cost on the JVM

context