skip to content

How does the JVM implement dynamic dispatch (virtual vs non-virtual invocation), and how does the JIT optimize it?

level: seniorimportance: should knowfreq 38%

answer

  1. invokestatic/invokespecial = non-virtual; invokevirtual/invokeinterface = virtual
  2. vtable: object→Klass→fixed slot index→method pointer
  3. Interfaces use itables/inline caches (no single fixed slot)
  4. JIT profiles receivers: mono/bimorphic → guarded inlining
  5. CHA inlines single-implementation methods, deopt if new subclass loads

basics

~20 s

Virtual method calls (invokevirtual/invokeinterface) look up the right override at run time, usually through a per-class method table. Non-virtual calls (invokestatic/invokespecial) have a fixed target. The JIT often removes the lookup by inlining when a call site sees only one or two types.

solid answer

~50 s

The JVM distinguishes virtual from non-virtual invocation at the bytecode level. Non-virtual calls — invokestatic (statics), invokespecial (constructors, private, super) — have a fixed, compile-time-resolved target. Virtual calls — invokevirtual for class-typed references and invokeinterface for interface-typed ones — defer the target to run time. The classic implementation is a per-class virtual method table (vtable): each method has a fixed slot index, and dispatch reads the object's class pointer, indexes the vtable, and jumps. Interface dispatch is trickier (a class can implement many interfaces at different slots), so JVMs use itables or inline caches. Crucially, the JIT compiler then optimizes most call sites: it profiles observed receiver types and, for monomorphic (one type) or bimorphic (two types) sites, performs guarded inlining — inlining the override behind a cheap class-check guard, with a slow-path fallback. So most 'virtual' calls cost almost nothing in practice; only truly megamorphic sites pay full dispatch.

code

java · 14 lines
java
interface Shape { double area(); }
class Circle implements Shape { public double area() { return Math.PI; } }
class Square implements Shape { public double area() { return 4.0; } }

static double total(java.util.List<Shape> shapes) {
    double sum = 0;
    for (Shape s : shapes) {
        sum += s.area(); // invokeinterface: dynamic dispatch.
        // If the JIT only ever sees Circle here, it inlines Circle.area()
        // behind a class-check guard (monomorphic). Mix Circle+Square and it
        // becomes bimorphic; many types -> megamorphic, real itable dispatch.
    }
    return sum;
}

go deeper

for a junior

Aware that virtual calls pick the override at run time and that the JVM, not the source code, decides the target.

for a middle

Knows the four invoke bytecodes split into virtual vs non-virtual and that a method table is used for dispatch.

for a senior

Explains vtable slot indexing, why interfaces need itables/inline caches, and JIT monomorphic/bimorphic guarded inlining.

for a principal

Reasons about CHA-driven devirtualization, deoptimization on new class load, call-site shape (mono/mega-morphic) impact, and when to rely on final vs trust the JIT.

## Virtual vs non-virtual at the bytecode level The Java compiler chooses one of four invoke bytecodes: - `invokestatic` — a `static` method. Target fixed at compile/link time. **Non-virtual.** - `invokespecial` — constructors, `private` methods, and `super.m()` calls. Target fixed (no override lookup). **Non-virtual.** - `invokevirtual` — an ordinary instance method through a class-typed reference. Target chosen at run time from the receiver's actual class. **Virtual.** - `invokeinterface` — an instance method through an interface-typed reference. **Virtual**, but the receiver's class must be searched for the interface's method. (There is also `invokedynamic`, used for lambdas/string-concat/etc., which bootstraps a call site once — out of scope here.) ## The vtable: how single-inheritance virtual dispatch works Under single implementation inheritance, the JVM can lay out each class's methods so that an overridable method always occupies the **same slot index** in every subclass's table: 1. The object header has a pointer to its **class metadata** (Klass). 2. That metadata holds a **virtual method table (vtable)** — an array of method entry pointers. 3. `invokevirtual m` becomes: load the object's class pointer → read vtable[slot_of_m] → call it. Because a subclass that overrides `m` puts its own override in the *same* slot, the indexed read automatically lands on the most-derived implementation. This is constant-time and is exactly what makes dynamic binding cheap structurally. ## Why interfaces are harder A class can implement many interfaces, and a given interface method can't get a single consistent slot across all implementers (the slots collide). So `invokeinterface` can't use one fixed index. JVMs use one of: - **itables** (interface method tables) — a per-class table searched by interface, then indexed. - **Inline caches** — remember the last receiver class at the call site and skip the search when it repeats. This is why `invokeinterface` is, in the abstract, slightly costlier than `invokevirtual` — though the JIT usually erases the difference. ## JIT devirtualization — where the real performance comes from The interpreter and C1/C2 (HotSpot) collect **type profiles** at each call site: which receiver classes actually showed up. Based on that: - **Monomorphic** site (only one type ever seen): the JIT inlines that single target behind a **guard** (`if receiver.class == ProfiledClass`). If the guard holds (the common case), the call is replaced by the inlined body — zero dispatch cost. If it fails, it deoptimizes to a slow path. - **Bimorphic** (two types): two guarded inlines. - **Polymorphic/megamorphic** (many types): falls back to a real vtable/itable dispatch or a polymorphic inline cache; not inlined. - **CHA (Class Hierarchy Analysis):** if, at compile time, only one implementation of a method is loaded in the whole hierarchy, HotSpot can inline it *unconditionally* (with a dependency that triggers deoptimization if a new overriding class is later loaded). This is why `final` (and effectively-final, single-implementation) methods are reliably devirtualized, and why "virtual calls are slow" is mostly a myth on a warmed-up JIT. ## Worked picture ```java interface Shape { double area(); } class Circle implements Shape { public double area() { return 3.14; } } Shape s = new Circle(); s.area(); // invokeinterface; JIT sees only Circle -> inlines area() behind a class guard ``` At the bytecode level it's a virtual interface call; after JIT warmup at a monomorphic site, it's effectively a constant. ## Takeaways for design and reasoning - Virtual dispatch is structurally O(1) (vtable) and usually optimized away (inlining) — don't contort designs to avoid it. - Keeping call sites monomorphic (few concrete types) helps the JIT; very megamorphic sites are the ones that pay. - `final`/private/static give the JVM a fixed target and the easiest devirtualization, but the gain on hot code is often marginal because CHA already handles single-implementation cases.

  • Why is invokeinterface harder to optimize than invokevirtual?
    A class implements many interfaces, so an interface method can't occupy a single consistent vtable slot across all implementers. The JVM must use an itable search or inline cache instead of a fixed index, though JIT inline caching usually removes the cost.
  • How can the JIT inline a virtual call that has no final/static marker?
    Via type-profile-guided inlining (monomorphic/bimorphic guarded inlining) and Class Hierarchy Analysis: if only one implementation is loaded, it inlines unconditionally with a deoptimization guard that fires if a new overriding class is later loaded.

saying these in an interview costs you the question

  • Claiming every virtual call walks the class hierarchy at run time (vtable is O(1); JIT often inlines)
  • Saying interface and class dispatch are identical in mechanism
  • Assuming virtual calls are always a big performance cost on hot paths
  • Confusing invokedynamic (lambdas) with normal virtual dispatch

context