A Java application starts and runs for minutes, then throws NoSuchMethodError deep in a rarely used branch. Explain, in terms of how the JVM turns constant-pool symbolic references into direct references, why the failure surfaced only then.
answer
- Class file holds names, not addresses
- Resolve: find class → lookup by name+descriptor → access check → direct ref
- HotSpot resolves at first execution of the instruction
- Cold path = still-unresolved entry
- Resolution failures are remembered, not retried
basics
~20 sClass files reference members symbolically — by name and descriptor in the constant pool. HotSpot resolves each reference lazily, on first execution of the instruction that uses it. The rare branch's method reference was never resolved before, so the mismatch between the compiled-against and loaded classes only surfaced when the branch finally ran.
solid answer
~50 sCompiled bytecode never contains addresses. A call is a `Methodref` constant-pool entry naming a class, a method name and a descriptor. Before the instruction can execute, the JVM must resolve that symbol: load or find the named class, run the specification's method-lookup algorithm through the class, its superclasses and superinterfaces, check accessibility, and replace the symbol with a direct reference — a vtable or itable index, or a pointer to the method metadata — cached so later executions skip the work. The specification permits eager or lazy resolution, and HotSpot chooses lazy: an entry is resolved the first time its instruction executes. So a class can load, verify, initialize and run for a long time while some of its constant-pool entries are still unresolved. A library upgrade that removed or changed a method's signature therefore stays invisible until a code path touching it runs. The result is a `NoSuchMethodError` at that moment; resolution failures are also recorded, so retrying reports the same error rather than re-attempting the lookup.
code
text · 9 lines// javap -c -v
17: invokevirtual #23 // Method com/lib/Foo.bar:(I)V
Constant pool:
#23 = Methodref #7.#24 // com/lib/Foo.bar:(I)V
#7 = Class #31 // com/lib/Foo
#24 = NameAndType #32:#33 // bar:(I)V
// Nothing above is looked up until instruction 17 first executes.go deeper
Say the class file refers to methods by name, and the JVM only looks the name up the first time that line runs, which is when a mismatch shows up.
Describe the lookup by name plus descriptor and the replacement with a direct reference cached in the runtime constant pool.
Explain lazy versus eager resolution as a specification-permitted choice, connect the error family to first use, and propose start-up self-checks or cold-path tests to move detection earlier.
Treat this as a release-engineering property: binary compatibility failures surface on first use, so make dependency upgrades verified by exercising cold paths, and weigh the start-up cost of eager checks against the risk of late failure.
## Symbolic references: what is really in the class file When javac compiles `list.add(x)`, it does not know where `add` will live in memory — it cannot, since the implementation is chosen at run time by whichever class file is on the path. So the class file records a **symbolic reference**: a `CONSTANT_Methodref` pointing at a `CONSTANT_Class` (the declared owner's name) and a `CONSTANT_NameAndType` (the method name plus its type descriptor). Field and interface-method references have the same shape. The whole class file is written in names. ## What resolution does Resolution converts one such symbol into a **direct reference** the execution engine can use immediately. For a method that is an index into a virtual or interface method table, or a pointer to the method's runtime structure; for a field it is an offset within the object or within the static area. The steps are specified: 1. **Resolve the owning class** by name, using the referencing class's defining loader as the initiating loader. This may trigger loading, which may itself fail. 2. **Look the member up** using the specification's algorithm — for methods: the class itself, then superclasses, then the maximally specific superinterface method; for fields: the class, then its superinterfaces, then superclasses. The match is on name **and descriptor**, so a changed parameter or return type is a different method entirely. 3. **Check access** from the referencing class, including module readability and package export rules. 4. **Record the direct reference** in the runtime constant pool (HotSpot keeps a constant-pool cache and rewrites the bytecode to a fast form), so subsequent executions cost a load. ## Eager versus lazy, and why the choice is visible The specification allows an implementation to resolve everything during linking or to defer each reference until its instruction first executes, provided the observable behaviour — including *when* errors are thrown — matches the lazy model closely enough. HotSpot resolves lazily. That single decision explains the scenario: a class whose constant pool references `Foo.bar(int)` loads, verifies, prepares, initializes and executes normally, because the entry for `bar` is untouched until an instruction using it runs. When a dependency upgrade replaced `bar(int)` with `bar(long)`, the class still compiles-against-old, runs-against-new perfectly — until the rare branch executes, resolution runs the lookup, finds no method with that exact descriptor, and throws `NoSuchMethodError`. The same laziness produces the neighbouring symptoms: a missing class referenced only in a cold path yields `NoClassDefFoundError` at that moment; an access that became illegal yields `IllegalAccessError`; a field whose type changed yields `NoSuchFieldError`. All are resolution outcomes, delivered at first use. Resolution failures are also **remembered**: once a reference fails to resolve, the JVM records that failure and reports the same error on subsequent attempts rather than repeating the lookup, so retry loops do not eventually succeed. ## invokedynamic: resolution with a hook `invokedynamic` generalises this. Each `invokedynamic` instruction has its own call site, unresolved until first execution; then the JVM runs the site's bootstrap method, which returns a `CallSite` holding a `MethodHandle` target, and that target is linked in. Lambdas and string concatenation use it, which is why the machinery behind a lambda is materialised the first time that specific lambda's instruction runs, not when the class loads. ## Practical consequences - **Failure timing is not failure absence.** "It started fine" says nothing about binary compatibility; cold paths carry unresolved references indefinitely. - **Test cold paths, or force the issue.** Integration tests exercising rare branches, or a startup self-check that touches critical APIs, converts a 3 a.m. `NoSuchMethodError` into a deploy-time failure. - **Recompile consumers on signature changes.** Descriptor matching is exact; a source-compatible change like widening a parameter type is not binary-compatible. - **Load-time cost is spread out**, which is a real benefit: eager resolution would force loading the transitive closure of every referenced class at start-up.
- Would eager resolution have caught this at start-up, and what would it cost?Largely yes: resolving every constant-pool entry at link time would surface missing or changed members immediately. The cost is that it forces loading the transitive closure of everything referenced, including code paths the process will never take, inflating start-up time and footprint. The specification permits it, but HotSpot's lazy strategy trades early detection for start-up cost.
- Why does a source-compatible library change still break a caller that was not recompiled?Because resolution matches on the exact descriptor, not on assignability. Changing a parameter from int to long, or a return type, produces a different descriptor, so the old caller's symbolic reference matches nothing and resolution throws NoSuchMethodError. Source compatibility and binary compatibility are separate properties, and only recompiling the caller restores the match.
A contact list stores names, not addresses. You only discover that someone has moved when you actually try to visit them — and the friend you never visit stays wrong in your list indefinitely.
saying these in an interview costs you the question
- Believing a clean start-up proves all references are valid
- Thinking descriptors are matched loosely, so a widened parameter type still binds
- Expecting a retry to succeed after a resolution error
- Confusing resolution with class loading — a class can be loaded while its references are unresolved
- Assuming the JVM must resolve everything during linking