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.

Advertisement

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 int

The 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.

Advertisement

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.

From name to machine code: the method handle pipelineLookupaccess rights of the callerfindVirtual / findVarHandleclass, name, MethodTypeMethodHandletyped, immutable referenceCombinatorsbindTo, filter, guardWithTeststatic final fieldor a ConstantCallSiteinvokeExact at call sitesymbolic type must matchJIT sees a constant handleinlines through the handle like a direct callHandle in a non-final field or a mapno constant folding: an indirect callAccess is checked once, at lookup time; invocation carries no further security checks.
Access is checked once at lookup; speed depends on whether the JIT can treat the handle as a constant.

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.

CombinatorWhat it doesTypical use
bindTo(x)Fix the first argument (usually the receiver)Turn a virtual method into a supplier for one object
insertArgumentsFix arguments at any positionPartial application of configuration
dropArgumentsAccept and ignore extra argumentsFit a handle into a wider signature
filterArgumentsPre-process arguments through other handlesParse or convert inputs
filterReturnValuePost-process the resultWrap, validate or convert outputs
guardWithTestIf test(args) then target else fallbackInline caches, fast paths
catchExceptionRoute a thrown exception to a handlerDefaults on failure
asTypeAdapt to a new MethodTypePrepare 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.

ModeMethodsGuarantee
Plainget, setLike an ordinary field access; no ordering beyond program order on this thread
OpaquegetOpaque, setOpaqueCoherent per variable, not torn or eliminated; no ordering of other variables
Acquire / releasegetAcquire, setReleaseA release write publishes earlier writes to a thread that acquire-reads it
VolatilegetVolatile, setVolatileSame as a volatile field: totally ordered
Atomic updatecompareAndSet, compareAndExchange, getAndAdd, getAndSet, getAndBitwiseOrRead-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

SymptomCauseFix
WrongMethodTypeExceptionCall-site types differ from the handle type, often a missing return castCast the result; asType the handle once
IllegalAccessException at lookupLookup lacks rights, or the package is not opened to the caller moduleUse the owning class's lookup, or opens plus privateLookupIn
Handle path slower than reflectionHandle stored in a non-final field or built per callstatic final field or ConstantCallSite
Unexpected boxing allocationsinvoke, or a VarHandle call with mismatched static typesinvokeExact; use withInvokeExactBehavior() (Java 16) on VarHandles to catch mismatches
UnsupportedOperationException on a VarHandleWrite mode on a final field, or an unsupported mode for the typeCheck the field and accessModeType before use
Rare wrong values under loadPlain or opaque mode where acquire/release was requiredUse the weakest mode that is provably correct; test with jcstress

What to do next

  1. Find reflective calls on hot paths and replace them with handles held in static final fields.
  2. Build each handle to the exact type you call it with, and call it with invokeExact.
  3. Replace AtomicReference or AtomicInteger fields in high-allocation classes with a VarHandle on a volatile field.
  4. Write down the access mode each concurrent field needs and why; default to volatile until a weaker mode is justified.
  5. Add jcstress tests for every class that uses acquire, release, opaque or weak CAS.
  6. When combinator chains pass about six steps, consider a hidden class instead.
  7. Read the java.lang.invoke package documentation for MethodHandles and VarHandle once end to end.
Key takeaway: A MethodHandle is a typed, access-checked-once reference to code; a VarHandle is the same idea for variables, with per-access memory-ordering modes. Obtain them from a Lookup that carries the right access, store them in static final fields so the JIT can inline through them, call them with invokeExact and exact static types, compose behaviour with combinators instead of generated bytecode, and choose the weakest VarHandle mode that is provably correct, from plain through acquire/release to volatile and atomic compare-and-set.