Skip to content

Virtual Methods

opencode-agent[bot] edited this page May 10, 2026 · 3 revisions

Virtual Methods Dispatch

Method dispatch system for virtual, interface, and static methods in JNode.

Overview

JNode implements a layered method dispatch system that resolves method calls at runtime by consulting per-class metadata structures stored in the TIB (Type Information Block). Every object carries a TIB reference in its header, and the TIB contains three dispatch mechanisms: the vtable for class-inheritance virtual calls, the IMT (Interface Method Table) for interface calls, and the CompiledIMT for architecture-specific fast dispatch. Static methods bypass all of these and are resolved at link time through the constant pool.

The dispatch pipeline works as follows: at link time, VmClassType.prepareTIB() builds a vtable by inheriting from the superclass and appending or overriding with this class's methods. Simultaneously, prepareIMT() builds an IMT by collecting all interface methods and inserting them into a 64-element hash table. The JIT compilers use precomputed vtable slot offsets (from VmInstanceMethod.getTibOffset()) to emit direct memory loads for virtual calls, and use the IMT's selector % 64 hash for interface calls. The CompiledIMT layer further compiles the IMT into machine code for optimization.

Understanding virtual method dispatch is essential for debugging method resolution issues, working with the JIT compilers, and comprehending how JNode achieves performance competitive with standard JVMs.

Key Components

Class / File Role
core/src/core/org/jnode/vm/classmgr/VmClassType.java Implements prepareTIB() and prepareIMT() for class vtable and IMT construction
core/src/core/org/jnode/vm/classmgr/TIBBuilder.java Builds the vtable from a superclass's TIB via inheritance-copy, method append, and override
core/src/core/org/jnode/vm/classmgr/IMTBuilder.java Builds the IMT with a fixed 64-element hash table and collision chaining
core/src/core/org/jnode/vm/classmgr/TIBLayout.java Defines TIB array index constants: VMTYPE_INDEX, IMT_INDEX, FIRST_METHOD_INDEX, etc.
core/src/core/org/jnode/vm/classmgr/ObjectLayout.java Defines IMT_LENGTH = 64 as the fixed hash table size
core/src/core/org/jnode/vm/classmgr/VmType.java Abstract base that coordinates TIB creation during class preparation
core/src/core/org/jnode/vm/classmgr/VmNormalClass.java Concrete class representation used in vtable inheritance
core/src/core/org/jnode/vm/classmgr/VmInstanceMethod.java Instance method representation with setTibOffset() for JIT dispatch
core/src/core/org/jnode/vm/classmgr/VmInterfaceClass.java Interface class representation used in IMT population
core/src/core/org/jnode/vm/compiler/CompiledIMT.java Abstract base for architecture-specific compiled IMT
core/src/core/org/jnode/vm/compiler/IMTCompiler.java Abstract base for architecture-specific IMT compilation

How It Works

Object → TIB Relationship

Every object in the JNode heap carries a two-slot header:

Object header (in heap):
  [offset -2] Flags slot   (GC mark, sync state, hashcode)
  [offset -1] TIB reference → points to the class's TIB array

The TIB is an Object[] array stored separately from the heap object. The TIB reference is a single pointer, so the object header stays compact at 2 slots.

TIB Array Layout

The TIB is organized at fixed indices per TIBLayout:

TIB[index] Layout:
  [0] VMTYPE_INDEX         → VmType (the class object itself)
  [1] IMT_INDEX            → Object[] (Interface Method Table, 64 elements)
  [2] IMTCOLLISIONS_INDEX  → boolean[] (collision flags for IMT slots)
  [3] COMPILED_IMT_INDEX   → Object (architecture-specific CompiledIMT, may be null)
  [4] SUPERCLASSES_INDEX   → VmType[] (superclass chain for GC)
  [5+] FIRST_METHOD_INDEX  → VmInstanceMethod[] (vtable, starts here)

The vtable begins at index 5 and grows by appending new methods as subclasses are loaded. See TIB for full TIB construction details.

vtable Construction

The vtable is built by VmClassType.prepareTIB() using a TIBBuilder:

protected Object[] prepareTIB(HashSet<VmInterfaceClass<?>> allInterfaces) {
    final VmNormalClass superClass = getSuperClass();
    final TIBBuilder vmt;

    final int tc_mtable_length = getNoDeclaredMethods();
    if (superClass != null) {
        // Inherit from superclass vtable
        vmt = new TIBBuilder(this, superClass.getTIB(), tc_mtable_length);
    } else {
        // Root class (e.g., Object) starts with empty table
        vmt = new TIBBuilder(this, tc_mtable_length);
    }

    // Process each declared instance method
    for (int i = 0; i < tc_mtable_length; i++) {
        final VmMethod mts = getDeclaredMethod(i);
        if (!(mts.isStatic() || mts.isSpecial())) {
            final VmInstanceMethod method = (VmInstanceMethod) mts;
            final int index = vmt.indexOf(name, signature);
            if (index >= 0) {
                // Override if visibility allows (public/protected/same-package)
                if (vmt.overrides(index, method)) {
                    vmt.set(index, method);
                } else {
                    vmt.add(method); // Package-private: add as new slot
                }
            } else {
                // New method: append to vtable
                vmt.add(method);
            }
        }
    }

    // For abstract classes: clone unimplemented interface methods into vtable
    if (isAbstract()) {
        for (VmInterfaceClass<?> icls : allInterfaces) {
            for (int j = 0; j < icls.getNoDeclaredMethods(); j++) {
                final VmMethod intfMethod = icls.getDeclaredMethod(j);
                if (!intfMethod.isStatic()) {
                    final int index = vmt.indexOf(intfMethod.getName(), intfMethod.getSignature());
                    if (index < 0) {
                        // Clone with this class's vtable offset
                        final VmInstanceMethod clone = new VmInstanceMethod(method);
                        vmt.add(clone);
                    }
                }
            }
        }
    }

    this.tib = vmt.toArray();
    return tib;
}

TIBBuilder maintains a HashMap<String, Integer> (name+signature → vtable index) for O(1) lookup during construction. See vtable for detailed construction rules and override vs. append semantics.

IMT Construction

The IMT is built by VmClassType.prepareIMT() using an IMTBuilder:

protected IMTBuilder prepareIMT(HashSet<VmInterfaceClass<?>> allInterfaces) {
    final IMTBuilder imtBuilder = new IMTBuilder();
    for (VmType<?> intf : allInterfaces) {
        final int max = intf.getNoDeclaredMethods();
        for (int m = 0; m < max; m++) {
            final VmMethod intfMethod = intf.getDeclaredMethod(m);
            if (!intfMethod.isStatic()) {
                final VmMethod clsMethod = getMethod(intfMethod.getName(), intfMethod.getSignature());
                if (clsMethod instanceof VmInstanceMethod) {
                    imtBuilder.add((VmInstanceMethod) clsMethod);
                } else {
                    throw new ClassFormatError(...);
                }
            }
        }
    }
    return imtBuilder;
}

The IMTBuilder uses a fixed 64-element hash table (ObjectLayout.IMT_LENGTH). Method lookup uses selector % 64 where selector is a precomputed hash of method name+signature. See IMT for collision handling details.

Static Method Dispatch

Static methods are not placed in the vtable. They are resolved at link time through the constant pool and compiled as direct calls to a fixed address. No runtime dispatch is needed because:

  • A static method belongs to a specific class, not an instance
  • The declaring class is known from the invokestatic bytecode constant pool entry
  • Static methods cannot be overridden (only hidden by subclass declarations)

JIT Compilation

The JIT compilers use the dispatch tables to emit efficient machine code:

Virtual call (invokevirtual):

// L1 compiler: load method pointer from vtable at precomputed offset
mov reg, [object_ref + vtableOffset]  // TIB + vtableIndex * wordSize
call reg

The vtable offset is known at compile time from methodRef.getVtableOffset().

Interface call (invokeinterface):

// Compute hash: index = selector % 64
mov eax, selector
and eax, 63  // selector & (64-1) == selector % 64
// Load IMT from TIB[1], then lookup
mov edx, [object_ref - 1]  // TIB reference
mov edx, [edx + 1 * wordSize]  // IMT array
mov edx, [edx + eax * wordSize]  // IMT[selector & 63]
call edx

If the slot has a collision, a linear search of the collision list follows. The CompiledIMT provides an optimized machine-code version that avoids this.

Inlining implications: The JIT can only inline a virtual call if it can devirtualize it (resolve the concrete class at compile time). When the exact type is unknown, the dispatch table lookup prevents inlining. The JIT's type profiling and inline caches track receiver types to enable speculative devirtualization. See JIT-Compilers.

Class Preparation Sequence

The complete dispatch table construction sequence during class preparation:

  1. VmType.doPrepare() collects all implemented interfaces via getAllInterfaces()
  2. prepareTIB() builds the vtable by copying the parent's TIB and adding/overriding methods
  3. prepareIMT() builds the IMT by hashing interface method selectors
  4. loader.compileIMT() creates the CompiledIMT for fast dispatch
  5. TIB is finalized and stored as an immutable Object[]

Gotchas & Non-Obvious Behavior

  • Vtable size is class-specific: Each class has its own vtable sized to its declared and inherited virtual methods. The JIT must resolve the vtable index relative to the object's actual class's TIB, not a static declaration.
  • IMT is class-specific: Each class builds its own IMT from its implemented interfaces. Different classes may have different collision patterns even for the same interface.
  • Collision lists break O(1): The IMT's hash table provides O(1) first probe, but collision lists require linear search. As more interface methods are added, collision probability increases.
  • CompiledIMT is optional: If TIB[3] (COMPILED_IMT_INDEX) is null, the runtime uses Java-side IMT lookup. Some architectures or build configurations may not generate it.
  • Selector is stable but not sequential: The selector is a precomputed hash of method name+signature, consistent across the entire VM. However, it is not a compact sequential index, so IMT lookup requires hash computation at call time.
  • Inlining decisions depend on dispatch: The JIT cannot inline virtual calls when the receiver type is unknown. Type profiling tracks concrete types for speculative devirtualization. Interface calls have additional dispatch overhead compared to virtual calls.
  • Synthetic abstract method cloning: Abstract classes implementing interfaces clone each unimplemented interface method into the vtable to prevent vtable offset conflicts across multiple abstract classes.
  • Static methods bypass dispatch entirely: Static methods use direct constant-pool resolution, not the vtable or IMT. This is why invokestatic is generally faster than invokevirtual.

Related Pages

  • vtable — Deep-dive on vtable construction, slot assignment, override vs. append rules
  • IMT — Deep-dive on interface method dispatch, hash collision handling, CompiledIMT
  • TIB — TIB array structure, construction, and runtime access
  • Object-Layout — Object header structure including TIB reference
  • Type-System-Internals — VmType hierarchy, class loading, and linking pipeline
  • JIT-Compilers — How compilers use dispatch tables for method calls and inlining
  • Memory-Management — GC interaction with TIB and method tables
  • Architecture — VM layer overview

Clone this wiki locally