5.2 Loading, Linking, and Invocation: From Symbolic References to invokedynamic
Bytecode calls usually refer to symbols in the constant pool. The JVM resolves names and descriptors, checks access, and, where required, selects an implementation from the receiver's runtime type. A name is a useful lead; the loader and dispatch rules still have work to do.
Loading, Linking, and Initialization Are Not One Thing
The lifecycle of a class or interface can be understood in three stages:
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. Static field initializers and static blocks are typically compiled into <clinit> and executed during initialization. Constant variables may instead receive values through ConstantValue attributes without assignments in <clinit>.
Save this as InitializationDemo.java, compile with javac --release 17 InitializationDemo.java, and run java InitializationDemo:
public final class InitializationDemo {
static final class Settings {
static final int DEFAULT_RETRIES = 3;
static int retries = loadRetries();
static int loadRetries() {
System.out.println("initialize");
return 3;
}
}
public static void main(String[] args) throws Exception {
Class.forName(Settings.class.getName(), false,
InitializationDemo.class.getClassLoader());
System.out.println(Settings.DEFAULT_RETRIES);
System.out.println("before active use");
System.out.println(Settings.retries);
System.out.println(Settings.retries);
}
}The output is 3, before active use, initialize, 3, then 3. The class literal and Class.forName(..., false, ...) do not request initialization, and reading a compile-time constant does not trigger it either. Reading the nonconstant static field retries does. After successful initialization, reading it again does not initialize the same runtime class a second time. Array classes and interfaces have additional rules; "any reference initializes the class" is too broad.
A Class's Identity Includes the Loader That Defined It
For ordinary classes and interfaces 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:
com.example.Plugin cannot be cast to com.example.PluginAlthough 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. A host's static cache that retains plugin objects, or a long-lived thread whose context class loader still points to a plugin loader, can keep the plugin and its loader reachable and prevent unloading. A plugin referencing its host is a different direction of retention. Trace paths from GC roots to the plugin instead of merely counting loaders.
What Each Invocation Instruction Expresses
| Instruction | Typical Use Case | Is Target Selected Dynamically by Call Site |
|---|---|---|
invokestatic | Static method | No |
invokespecial | Instance initialization, superclass calls, and other special dispatches | According to special rules |
invokevirtual | Instance method of a class | Yes |
invokeinterface | Interface method | Yes |
invokedynamic | Call site linked by a bootstrap method | Determined by call-site 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
The next two blocks are call-site fragments. Assume ConsolePrinter implements Printer, and place the two run methods inside the class that uses them.
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
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
A bytecode instruction that directly invokes an instance initialization method <init> must use invokespecial. This does not exclude higher-level construction APIs such as reflection or constructor method handles. Explicit superclass method calls and similar scenarios also follow special selection rules:
class Parent {
void refresh() { System.out.println("parent"); }
}
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.
invokedynamic instruction
↓ first linking
bootstrap method + name + method type + static parameters
↓
CallSite(target MethodHandle)
↓
target of subsequent callsOnce linked, subsequent invocations use the target handle, and the JVM can optimize according to the CallSite type. Some call sites allow target updates. Concurrent initial linking can also invoke the bootstrap method more than once, with only one result selected. A bootstrap method therefore is not an exactly-once entry point for business side effects.
Lambda and String Concatenation Are Just Common Use Cases
Save this complete example as LambdaDemo.java:
import java.util.function.Function;
public final class LambdaDemo {
public static void main(String[] args) {
Function<String, Integer> length = String::length;
String userId = args.length == 0 ? "ada" : args[0];
int count = length.apply(userId);
String message = "user=" + userId + ", count=" + count;
System.out.println(message);
}
}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:
String value = "hello" + "world";are folded into "helloworld" at compile time, with no runtime concatenation required.
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 use 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:
javac --release 17 LambdaDemo.java
javap -c -v -p LambdaDemoRunning java LambdaDemo prints user=ada, count=3. In the disassembly, inspect:
- The name and method descriptor of the constant pool item for
InvokeDynamic; - The
BootstrapMethodsattribute; - 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:
- Identify which five invocation instructions actually appear in the bytecode;
- Identify which calls require a receiver and how it enters the operand stack; static calls and
invokedynamichave no implicit receiver; - Locate the bootstrap method for an
invokedynamicinstruction; - Change the static type of a variable and observe whether the invocation instructions change;
- Predict, without running the program, which operations will trigger class initialization, then verify with logging.