Every JVM stack frame holds a reference to the run-time constant pool of the class that declares the executing method. What is that reference used for while the method runs?
answer
- bytecode carries pool indices, not addresses
- symbolic reference → resolve on first use → direct reference cached
- resolution ≠ initialization (<clinit> is separate)
- NoSuchMethodError at first execution, not at load
- invokedynamic: bootstrap method installs a CallSite once
basics
~20 sBytecode names other classes, fields and methods symbolically by constant-pool index, not by address. The frame's constant-pool reference is how an executing instruction turns index #7 into a real target — resolving it on first use, loading classes as needed, and caching the result. This is the JVM's dynamic linking.
solid answer
~60 sInstructions such as `invokevirtual #7`, `getfield #12` or `new #3` carry a **constant-pool index**, not an address. The frame's reference to the run-time constant pool of the current method's class is what lets the JVM look that index up. The entry is a **symbolic reference**: class name, member name, descriptor — text. On the *first* execution of that instruction, the JVM **resolves** it: load and link the referenced class if necessary, check it has the member, check access from the referring class, then convert the entry into a direct reference (a field offset, a vtable index, a method address). The result is cached in the pool, so later executions are cheap. Consequences that matter in practice: linkage errors such as `NoSuchMethodError` surface at *first use*, not at class load; a class is loaded lazily when a reference to it is first resolved; and `invokedynamic` extends this by resolving a call site once through a bootstrap method that installs a `CallSite`, which is how lambdas and string concatenation are linked.
code
text · 12 linesCode:
0: aload_1
1: invokeinterface #7, 1 // InterfaceMethod java/util/List.size:()I
6: ireturn
Constant pool:
#7 = InterfaceMethodref #21.#22 // java/util/List.size:()I
#21 = Class #23 // java/util/List
#22 = NameAndType #24:#25 // size:()I
// First execution of offset 1 resolves #7: load java.util.List if needed,
// find size()I, check access, install an itable-based direct reference, cache it.go deeper
Know that bytecode refers to other classes and members by symbolic name via constant-pool indices, and that the JVM turns those into real targets while running.
Describe first-use resolution and caching, and connect it to lazy class loading and to errors like NoSuchMethodError appearing mid-run.
Walk the resolution steps including access checking and consistent error re-throw, separate resolution from initialization, and explain the operational symptoms of version skew.
Discuss dynamic linking as the extension point it is — invokedynamic pushing linkage policy into libraries, and the loading/versioning risks that late binding creates in a large deployment.
## Symbolic references, and why they exist A compiled `.class` file cannot contain addresses. `javac` compiles each class separately, and the classes it refers to may be loaded from anywhere, by any class loader, in any order, possibly in a version different from the one compiled against. So the class file names its dependencies **symbolically** in the **constant pool**: an entry says "the method named `size`, descriptor `()I`, in the class `java/util/List`" — three pieces of text. Bytecode instructions that touch another class or member carry a pool index rather than an operand: `invokevirtual #7`, `getstatic #12`, `new #3`, `checkcast #9`, `ldc #4`. The **frame's reference to the run-time constant pool** of the current method's class is what turns that index into meaning — and it must be the *current class's* pool, because index #7 means something different in every class. When a class is loaded, its constant pool is turned into a **run-time constant pool**, a per-class structure that starts out holding those symbolic entries and gradually accumulates resolved results. ## Resolution: what happens on first execution The first time an instruction referencing pool entry #7 executes, the JVM performs **resolution**: 1. **Load** the referenced class if it is not loaded, using the defining loader of the *referring* class as the initiating loader. This is why loading is lazy and why loader delegation shows up here. 2. **Find** the member by name and descriptor, searching the class, then its superclasses, then its superinterfaces, per the lookup rules for the relevant kind of reference. 3. **Check access**: is the referring class permitted to see this class/member (public/protected/package-private/private, plus module readability and exports)? 4. **Install a direct reference** — an instance field offset, a static field address, a vtable/itable index, or a method pointer — and cache it in the pool entry. Every subsequent execution of that instruction hits the cached direct reference and skips all of this. Resolution is also required to be **consistent**: if it fails, the same error is thrown again on every later attempt, rather than the JVM retrying and possibly succeeding. Resolution is deliberately separate from **initialization**. Resolving a symbolic reference to a class does not run its static initializer; that happens on first *active use* — an instance creation, a static field access, a static method call. That is why you can see a class loaded long before `<clinit>` runs. ## What this explains in day-to-day work - **Linkage errors appear at first use, not at startup.** Recompile a library so a method's signature changes, drop it in without recompiling the caller, and the caller's class loads fine; the `NoSuchMethodError` fires the first time that call executes. This is the classic "it started up fine and failed 40 minutes in" bug. - **`NoClassDefFoundError` versus `ClassNotFoundException`**: the former typically comes from resolution failing (or a previously failed initialization) during execution; the latter from an explicit reflective lookup. - **Lazy loading is a consequence of lazy resolution.** A class mentioned only inside a rarely taken branch may never be loaded at all. - **Access is checked at link time, not just by the compiler.** Hand-edited or generated bytecode calling a private member of another class fails at resolution with `IllegalAccessError`. ## `invokedynamic`: resolution you can program The four classic invoke instructions have fixed resolution rules. `invokedynamic` generalises the mechanism: its pool entry names a **bootstrap method** plus static arguments. The first time the instruction executes, the JVM calls the bootstrap method, which returns a `CallSite` holding a `MethodHandle`; that call site is linked into the instruction and used for every subsequent execution. The linkage decision is thus made by *library code at run time*, which is how Java lambdas (`LambdaMetafactory` spinning the implementation class on first use), string concatenation (`StringConcatFactory`), and record `equals`/`hashCode`/`toString` are linked — with no change to the JVM for each new feature. ## The frame's role, precisely The frame does not own the pool; the class does. What the frame supplies is **which** pool is current, so that indices in the bytecode being executed are interpreted against the right class. When the frame is popped and the caller resumes, the current pool reverts to the caller's class's pool. In JIT-compiled code the indirection is usually gone — the compiler has already resolved and inlined the target, guarded by whatever assumptions it made — but the runtime keeps the mapping so that deoptimization can return to the interpreter and continue with the specification-level semantics. ## Summary Symbolic references keep class files independent of layout and load order. The frame's run-time constant pool reference is the binding between executing bytecode and those symbols: it resolves on first use, caches the direct reference, drives lazy class loading, enforces access at link time, and — through `invokedynamic` — lets libraries decide the target themselves.
- A service starts cleanly and then throws NoSuchMethodError on a code path hit an hour later. What does that tell you?That the failing call site was resolved for the first time only when that path executed — resolution is lazy and per-instruction, so a signature mismatch between the compiled caller and the class actually on the classpath surfaces at first use rather than at startup. The fix is a version alignment problem: the class present at run time does not have the member the caller was compiled against.
- Does resolving a symbolic reference to a class run that class's static initializer?No. Resolution loads, links and access-checks the class; initialization is a separate step triggered by first active use, such as creating an instance, reading or writing a static field, or invoking a static method. This is why a class can appear in the loaded-class list well before its `<clinit>` has run.
saying these in an interview costs you the question
- Saying all symbolic references are resolved eagerly when a class is loaded — resolution is lazy and per-call-site in practice.
- Treating resolution and initialization as the same step, so `<clinit>` is claimed to run on load.
- Claiming access checks are only a compile-time concern; the JVM re-checks them at resolution and can throw IllegalAccessError.
- Describing the constant pool as belonging to the frame rather than to the class, with the frame merely referencing it.
- Believing `invokedynamic` re-runs its bootstrap method on every invocation instead of linking a CallSite once.