Java has three ways to call code you do not know at compile time: reflection, method handles and generated bytecode. Method handles, in java.lang.invoke since Java 7, sit in the middle. A MethodHandle is a typed, directly executable reference to a method, constructor or field; a VarHandle, added in Java 9, is a typed reference to a variable that can be read and written with explicit memory-ordering semantics. Both are what the JDK itself is built on: lambdas are linked through invokedynamic and method handles, and since JDK 18 (JEP 416) core reflection is implemented on top of them.
This article explains the model from the ground up: how a handle is obtained and why access is checked once, why invokeExact is so strict, how combinators build behaviour without bytecode, what call sites are for, and how VarHandle access modes map to the memory model. When to prefer plain reflection is covered in Java Reflection, in depth.
Lookup: access is decided once
Every handle starts from a MethodHandles.Lookup, an object that carries the access rights of the class that created it. MethodHandles.lookup() returns a lookup with the caller's full rights: it can find private members of its own class, exactly as the source code could. MethodHandles.publicLookup() can only see public members of exported packages.
Access is checked when you call findVirtual, findStatic, findGetter or findVarHandle, not when you invoke. That is the key difference from reflection, which re-checks on every Method.invoke unless you call setAccessible. A handle is a capability: whoever holds it can call it. Pass handles, never your full-power lookup, to code you do not trust.
import java.lang.invoke.*;
import static java.lang.invoke.MethodType.methodType;
public final class Handles {
static final MethodHandle STRLEN;
static final MethodHandle CONCAT;
static final MethodHandle NEW_SB;
static {
var l = MethodHandles.lookup();
try {
STRLEN = l.findVirtual(String.class, "length", methodType(int.class));
CONCAT = l.findVirtual(String.class, "concat", methodType(String.class, String.class));
NEW_SB = l.findConstructor(StringBuilder.class, methodType(void.class, String.class));
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
}A MethodType is the full signature, return type first. For a virtual method the receiver becomes the first parameter of the handle, so STRLEN has type (String)int. Under modules, reaching a private member of another class needs MethodHandles.privateLookupIn(target, lookup) (Java 9), which succeeds only if the target's module opens the package to the caller. That is the same rule that governs deep reflection, and it is deliberate.
invokeExact, invoke and signature polymorphism
invokeExact and invoke are signature-polymorphic: the compiler does not check arguments against a fixed declaration but records the static types used at the call site as a symbolic type descriptor. At run time that descriptor is compared with the handle's type.
- invokeExact requires the descriptor to equal the handle's type exactly, including the return type. Any difference throws
WrongMethodTypeException. - invoke adapts when the types differ, as if by
asType: boxing, unboxing, widening and casts. Convenient, and slower when the adaptation cannot be cached.
int n = (int) Handles.STRLEN.invokeExact("hello"); // OK: (String)int
Object o = Handles.STRLEN.invokeExact("hello"); // WrongMethodTypeException: (String)Object
int m = (int) Handles.STRLEN.invokeExact((Object) "hi"); // WrongMethodTypeException: (Object)int
Object p = Handles.STRLEN.invoke("hello"); // OK: invoke boxes the intThe cast on the left matters because it is the return type the compiler writes into the descriptor; drop it and the descriptor ends in Object. Developers hit this as a mysterious exception on code that looks right. The cure is to build the handle to the exact shape you will call, with asType if needed, and then always call it with matching static types.
Why static final makes handles fast
The JIT can inline through a method handle only when the handle itself is a constant it can see while compiling. A handle in a static final field qualifies: HotSpot treats it as a constant, follows the handle's internal form and compiles the call as if it were direct. A handle in an instance field, a HashMap or a local built per call is opaque, and each invocation is an indirect call through generic code.
So the idioms are: build handles once, store them in static final fields, or behind a ConstantCallSite when generating code. If you genuinely need a handle chosen at run time, a MutableCallSite lets the JIT optimise for the current target and deoptimise when you change it, which is how dynamic languages on the JVM stay fast.
Combinators: behaviour without bytecode
The class MethodHandles is a small functional toolkit. Each method takes handles and returns a new handle, and the JIT sees through the whole composition when the root is constant.
| Combinator | What it does | Typical use |
|---|---|---|
bindTo(x) | Fix the first argument (usually the receiver) | Turn a virtual method into a supplier for one object |
insertArguments | Fix arguments at any position | Partial application of configuration |
dropArguments | Accept and ignore extra arguments | Fit a handle into a wider signature |
filterArguments | Pre-process arguments through other handles | Parse or convert inputs |
filterReturnValue | Post-process the result | Wrap, validate or convert outputs |
guardWithTest | If test(args) then target else fallback | Inline caches, fast paths |
catchException | Route a thrown exception to a handler | Defaults on failure |
asType | Adapt to a new MethodType | Prepare for invokeExact |
// A dispatcher: strings use a fast length path, everything else falls back to toString().length()
static final MethodHandle LENGTH_OF;
static {
try {
var l = MethodHandles.lookup();
MethodHandle isString = l.findVirtual(Class.class, "isInstance", methodType(boolean.class, Object.class))
.bindTo(String.class); // (Object)boolean
MethodHandle fast = Handles.STRLEN.asType(methodType(int.class, Object.class));
MethodHandle toStr = l.findVirtual(Object.class, "toString", methodType(String.class));
MethodHandle slow = MethodHandles.filterReturnValue(toStr, Handles.STRLEN); // (Object)int
LENGTH_OF = MethodHandles.guardWithTest(isString, fast, slow);
} catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); }
}
// int n = (int) LENGTH_OF.invokeExact((Object) value);This is exactly the structure of a monomorphic inline cache in a language runtime: a type test, a specialised path and a generic fallback. Combinator chains get hard to read quickly; past five or six steps, generating a small hidden class with Lookup.defineHiddenClass (Java 15) is often clearer.
Call sites and invokedynamic
An invokedynamic instruction has no fixed target. The first time it executes, the JVM calls a bootstrap method, which returns a CallSite holding a method handle; every later execution jumps through that call site. Lambdas use this: the bootstrap is LambdaMetafactory.metafactory, which spins a class implementing the functional interface. String concatenation since Java 9 uses StringConcatFactory the same way.
You meet call sites directly when writing a language runtime, a serializer that generates code, or an agent. ConstantCallSite never changes and is optimised like a static final handle; MutableCallSite can be retargeted with setTarget, at the price of deoptimising compiled callers. For the language-level view of lambda linkage see Java Lambda Expressions.
VarHandle: one variable, many access modes
A VarHandle refers to a variable: an instance field, a static field, an array element or a view of a byte array or buffer. Obtain one with findVarHandle, findStaticVarHandle, MethodHandles.arrayElementVarHandle or MethodHandles.byteArrayViewVarHandle. Like method handle invocation, its access methods are signature-polymorphic, so cast the result to the variable's exact type.
What makes it more than a faster reflective field is the access modes. The same variable can be read or written with different ordering guarantees, chosen per access rather than per declaration.
| Mode | Methods | Guarantee |
|---|---|---|
| Plain | get, set | Like an ordinary field access; no ordering beyond program order on this thread |
| Opaque | getOpaque, setOpaque | Coherent per variable, not torn or eliminated; no ordering of other variables |
| Acquire / release | getAcquire, setRelease | A release write publishes earlier writes to a thread that acquire-reads it |
| Volatile | getVolatile, setVolatile | Same as a volatile field: totally ordered |
| Atomic update | compareAndSet, compareAndExchange, getAndAdd, getAndSet, getAndBitwiseOr | Read-modify-write, volatile semantics unless the Acquire, Release or Plain variant is used |
Acquire and release are enough for single-producer handoff and are cheaper than volatile on weakly ordered hardware. The weak CAS forms (weakCompareAndSetPlain and friends) may fail spuriously and belong in retry loops. The ordering rules underneath are explained in Java Memory Model, and the classic wrappers that VarHandle largely replaces in library code in Java Atomic Classes. A VarHandle on a final field supports only read modes; attempting a write mode throws UnsupportedOperationException.
Worked example: a lock-free stack without wrapper objects
An AtomicReference field costs an extra object per instance and an extra pointer hop on every access. With a VarHandle, the head pointer is a plain field of the stack itself, and the atomic operations are applied to it.
import java.lang.invoke.*;
public final class TreiberStack<T> {
private static final class Node<T> {
final T value; Node<T> next;
Node(T value) { this.value = value; }
}
private volatile Node<T> head; // the only shared state
private static final VarHandle HEAD;
static {
try {
HEAD = MethodHandles.lookup().findVarHandle(TreiberStack.class, "head", Node.class);
} catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); }
}
public void push(T value) {
Node<T> n = new Node<>(value);
Node<T> h;
do {
h = head; // volatile read
n.next = h; // plain write, published by the CAS
} while (!HEAD.compareAndSet(this, h, n));
}
@SuppressWarnings("unchecked")
public T pop() {
Node<T> h, next;
do {
h = (Node<T>) HEAD.getAcquire(this);
if (h == null) return null;
next = h.next;
} while (!HEAD.weakCompareAndSet(this, h, next)); // retry loop tolerates spurious failure
return h.value;
}
}Walk the data flow. push writes n.next with a plain store, then publishes n with a CAS that has volatile semantics, so any thread that later reads head with acquire or stronger also sees n.next. pop reads with acquire, which is sufficient to see the node's fields, and uses a weak CAS because it is already in a loop. Two caveats. The receiver passed first must be a TreiberStack, or the call throws ClassCastException. And Java's garbage collector means this stack does not suffer the ABA problem that the same algorithm has in C, because a node cannot be freed and reused while a thread still holds a reference to it.
Test code like this with jcstress rather than unit tests; a stress harness explores interleavings that ordinary tests never hit.
Failure modes
| Symptom | Cause | Fix |
|---|---|---|
WrongMethodTypeException | Call-site types differ from the handle type, often a missing return cast | Cast the result; asType the handle once |
IllegalAccessException at lookup | Lookup lacks rights, or the package is not opened to the caller module | Use the owning class's lookup, or opens plus privateLookupIn |
| Handle path slower than reflection | Handle stored in a non-final field or built per call | static final field or ConstantCallSite |
| Unexpected boxing allocations | invoke, or a VarHandle call with mismatched static types | invokeExact; use withInvokeExactBehavior() (Java 16) on VarHandles to catch mismatches |
UnsupportedOperationException on a VarHandle | Write mode on a final field, or an unsupported mode for the type | Check the field and accessModeType before use |
| Rare wrong values under load | Plain or opaque mode where acquire/release was required | Use the weakest mode that is provably correct; test with jcstress |
What to do next
- Find reflective calls on hot paths and replace them with handles held in static final fields.
- Build each handle to the exact type you call it with, and call it with invokeExact.
- Replace AtomicReference or AtomicInteger fields in high-allocation classes with a VarHandle on a volatile field.
- Write down the access mode each concurrent field needs and why; default to volatile until a weaker mode is justified.
- Add jcstress tests for every class that uses acquire, release, opaque or weak CAS.
- When combinator chains pass about six steps, consider a hidden class instead.
- Read the java.lang.invoke package documentation for MethodHandles and VarHandle once end to end.