Skip to content

5.2 Loading, Linking, and Invocation: From Symbolic References to invokedynamic

Once a bytecode invocation instruction enters the runtime, it ultimately resolves to a specific class, method, and machine code segment, determined by the loading and dispatch process.

Bytecode method calls typically do not contain direct memory addresses but instead reference symbolic names stored in the constant pool. The JVM must resolve these names and descriptors into actual callable targets, while adhering to class loading rules, access control, and dynamic dispatch semantics.

Loading, Linking, and Initialization Are Not One Thing

The lifecycle of a class or interface can be understood in three stages:

text
Loading
  ↓ Read the binary representation, create a Class object
Linking
  ├─ Verification
  ├─ Preparation
  └─ Resolution (may be deferred per specification)
Initialization
  ↓ Execute the class or interface initialization method <clinit>

During the preparation phase, static fields are allocated memory and initialized to their defined default values, this does not mean that all static initializer expressions in the source code are executed. Assignments to static fields and static blocks in the source code are typically compiled into <clinit> and executed during initialization.

java
final class Settings {
    static int retries = loadRetries();

    static int loadRetries() {
        System.out.println("initialize");
        return 3;
    }
}

Simply detecting Settings.class in the classpath will not necessarily trigger its execution; initialization of <clinit> occurs only when an active reference to it is made. For array classes, interfaces, and compile-time constants, there are more nuanced rules, these should be referenced according to the JLS or JVMS, not generalized by the rule "one reference equals one initialization."

A Class's Identity Includes the Loader That Defined It

In the JVM, runtime type identity is not solely determined by the binary class name, it also depends on the class loader that defined it. Two different class loaders may define classes with the same binary name, such as com.example.Plugin, and these will be considered distinct at runtime, even if their bytecodes are identical.

This explains a common issue in plugin systems and application servers:

text
com.example.Plugin cannot be cast to com.example.Plugin

Although the class names appear identical in the error message, different class loaders define them. Diagnose the problem by examining the class name, its source, and the class loader hierarchy.

The class loader also governs namespace isolation and dependency visibility. Arbitrarily changing the thread context class loader, or allowing plugins to hold static references to classes loaded by the host loader, can lead to classes that cannot be unloaded and to long-term retention of metadata.

What Each Invocation Instruction Expresses

InstructionTypical Use CaseIs Target Selected Dynamically by Call Site
invokestaticStatic methodNo
invokespecialInstance initialization, superclass calls, and other special dispatchesAccording to special rules
invokevirtualInstance method of a classYes
invokeinterfaceInterface methodYes
invokedynamicCall point established by a guide methodDetermined by call point protocol

The source code syntax and generated instruction do not always map one-to-one. Compiler version, target class version, and access mode can all influence the final output. Therefore, actual bytecode output should be verified using javap.

invokevirtual: Dispatch Based on Runtime Receiver Type

java
interface Printer {
    void print(String text);
}

void run(ConsolePrinter printer) {
    printer.print("ready");
}

When the symbol reference at a call site refers to a class method, invokevirtual is typically used. Resolution first verifies the validity of the symbol reference; at runtime, the actual implementation is selected based on the receiver's runtime type.

invokeinterface: Invocation from Interface Reference

java
void run(Printer printer) {
    printer.print("ready");
}

When a call site references an interface method, invokeinterface is typically employed. Interface default methods and method resolution conflicts are defined by the specification and cannot be reduced to "always look up a fixed interface table." Specific JVM implementations may apply various caching and optimization strategies.

invokespecial: Bypassing Standard Virtual Dispatch Rules

Constructors <init> can only be invoked from invokespecial. Explicit superclass method calls and similar scenarios also follow special selection rules:

java
class Child extends Parent {
    Child() {
        super();
    }

    void refresh() {
        super.refresh();
    }
}

Do not assume that all private methods necessarily correspond to invokespecial. The relationship between source-level access modifiers and final bytecode has evolved over time and is also influenced by the specific call pattern. Determining the final compiled output should always be based on inspecting the generated bytecode.

invokedynamic Handing Linking Strategy to the Bootstrap Method

The linking semantics of the first four instructions are primarily predefined by the JVM. A constant pool entry for invokedynamic points to a bootstrap method and static parameters. At the first linking phase, the bootstrap method returns a CallSite, whose target MethodHandle determines the behavior of subsequent calls.

text
invokedynamic instruction
      ↓ first linking
bootstrap method + name + method type + static parameters

CallSite(target MethodHandle)

target of subsequent calls

It is not "re-finding via reflection on every call." Once a call site is established, the JVM can optimize based on the CallSite type and implementation strategy.

Lambda and String Concatenation Are Just Common Use Cases

java
Function<String, Integer> length = String::length;
String message = "user=" + userId + ", count=" + count;

Modern javac often uses invokedynamic in conjunction with a bootstrap method to enable lambda expressions and non-constant string concatenation. For example, constant expressions such as:

java
String value = "hello" + "world";

may be directly folded into constants at compile time.

These are compiler-generated strategies, not requirements in the Java language specification that all lambdas must be implemented using a specific bootstrap method. Other JVM-based languages can leverage invokedynamic to achieve dynamic method dispatch, runtime adaptation, and other distinct semantics.

Use the following command to locate call sites and examine the BootstrapMethods attribute:

bash
javac LambdaDemo.java
javap -c -v -p LambdaDemo

Pay close attention to:

  • The name and method descriptor of the constant pool item for InvokeDynamic;
  • The BootstrapMethods attribute;
  • The bootstrap method handle and its static parameters;
  • Whether the compiler generated additional synthetic methods.

Completion Check

Write a class containing static methods, instance overrides, interface calls, super calls, lambdas, and string concatenation. Then:

  1. Identify which five invocation instructions actually appear in the bytecode;
  2. For each invocation site, explain how the receiver enters the operand stack;
  3. Locate the method that serves as the entry point for an invokedynamic;
  4. Change the static type of a variable and observe whether the invocation instructions change;
  5. Predict, without running the program, which operations will trigger class initialization, then verify with logging.

References

Built with VitePress | Software Systems Atlas