MethodHandles, VarHandle, and invokedynamic Bytecode
- Last updated
- Prerequisites:
- Java reflection basics
- Understanding of the JVM bytecode and class files
- java
- methodhandles
- varhandle
- invokedynamic
- reflection
- bytecode
Read at your depth
The practical view
MethodHandle is a typed, directly-invocable reference to a method (or field accessor/constructor): MethodHandles.lookup().findVirtual(String.class, "length", MethodType.methodType(int.class)). MethodHandles are faster than reflection for repeated calls and can be combined (asType, bindTo, filterArguments). VarHandle (java.lang.invoke) gives typed access to fields and array elements with atomic/volatile access modes (compareAndSet, getAndAdd). invokedynamic is a bytecode instruction (used by lambdas, string concat, and language runtimes) that lets the runtime bootstrap the call target lazily and cache it. Lambda expressions compile to invokedynamic.
The same idea in other frameworks
N/A — JVM bytecode feature.N/A — JVM feature.N/A — JVM feature.N/A — JVM feature.Legacy vs modern
Reflection invoke vs MethodHandle invoke
Reflection resolves and checks every call; a MethodHandle is a pre-bound, typed, directly-callable reference that the JIT can inline.
Method m = Foo.class.getMethod("bar", int.class); // ❌ OLDMethodHandle bar = lookup.findVirtual(Foo.class, "bar", MethodType.methodType(void.class, int.class)); // ✅ NEWInterview gotchas
Context
This tests the bytecode-level dynamic linking model — interviewers want the bootstrap/link/cache mechanism and why it enables lazily-decided, cached call targets.
The mechanical answer
invokestatic/invokevirtual have a statically-known target (resolved at link time). invokedynamic defers the target decision to runtime: the call site names a bootstrap method (BSM); on first execution, the JVM calls the BSM, which returns a CallSite wrapping the target MethodHandle; the call site is then linked and cached, so subsequent executions are direct calls. This lets the runtime choose the implementation based on runtime conditions (e.g. which lambda factory, which string-concat strategy, which method in a dynamically-typed language) and lets the JIT treat the linked call as a normal direct/inlined call. For lambdas, the BSM returns a constant call site pointing to the generated lambda class's method, and the lambda instance (if stateless) can even be cached — eliminating per-creation allocation.
Trap
The naive answer is 'invokedynamic is just reflection at bytecode level'. It's a lazy, cached linking mechanism, not per-call reflection — the bootstrap runs once and the call site is bound. Another trap: assuming every invokedynamic allocates on each call — stateless lambdas use a constant call site and cached instances. Candidates who describe the BSM → CallSite → cache flow show real understanding.
Context
This probes the atomic/ordering semantics — interviewers want the distinction between plain/opaque/acquire/release/volatile modes and how they correspond to JMM guarantees.
The mechanical answer
VarHandle access modes cover plain reads/writes and read-modify-write operations (compareAndSet, getAndAdd, getAndSet, etc.) each available in several memory-ordering flavors: PLAIN (no ordering guarantees, like a normal field access), OPAQUE (prevents reordering relative to itself but gives no visibility guarantee), ACQUIRE/RELEASE (one-way ordering: acquire prevents subsequent operations from moving before it; release prevents prior operations from moving after it), and VOLATILE (full happens-before semantics like a volatile field). These map directly to the JMM's synchronization actions: VOLATILE creates the happens-before edges of a volatile access; ACQUIRE/RELEASE give the one-way edges used in lock-free structures; OPAQUE is for atomicity without ordering. The JIT lowers each mode to the appropriate CPU barriers/instructions.
Trap
The naive answer is 'VarHandle is just CAS'. It's a full matrix of access modes and ordering flavors beyond CAS. Another trap: using PLAIN for a flag read expecting visibility — PLAIN provides none; you need VOLATILE/ACQUIRE. Candidates who map modes to JMM edges show real concurrency depth.