invokedynamic
the bytecode that powers lambdas, string concat and records.
- chapter
- chapter 11.06
- reading time
- 9 min read
the other four invoke instructions name the method they call. invokedynamic names a bootstrap method instead. the first time the instruction runs, the JVM calls that bootstrap, which returns a CallSite holding a method handle. the instruction is linked to it, and every later execution calls the handle directly.
it's how java compiles lambdas, string +, records' toString/equals/hashCode and pattern switch, without baking the strategy into your class file.
the picture
you know how "hi " + name used to decompile to a StringBuilder chain, and on modern java it decompiles to... one weird instruction? that's this.
think of a phone number that doesn't belong to anyone yet. the first time someone dials it, the call goes to an operator (the bootstrap method), who looks at what you're trying to do, figures out the best person to handle it, and wires that number straight to them, for good. every call after that rings through directly; the operator is never involved again.
the point of the indirection: javac decides what the code means (concatenate these, implement Supplier with this method), but the how is decided at runtime, by JDK code that can get smarter in every release without you recompiling anything. the instruction was added in java 7, for dynamic languages on the JVM. java itself started using it in java 8. the five invoke instructions chapter has the comparison with the other four.
four call sites javac writes today
all four of these came out of one file compiled with javac 25.
the first time this instruction runs, it's unlinked. it names no method, just a bootstrap method (in the class's BootstrapMethods attribute), a name and a type.
javap -v output. watch step 5: once linked, the bootstrap is never called again.notice what's in the bootstrap arguments. for the lambda: a handle to the private method javac moved your lambda body into. for concatenation: a recipe string with \u0001 where each argument goes. for the record: the component names and getters for its fields. for the switch: the case types, in order. javac hands over the facts and the JDK writes the code. the full story of each lives in its own chapter: lambda internals, string concatenation, records, pattern matching.
| feature | since | bootstrap |
|---|---|---|
| lambdas and method refs | java 8 | LambdaMetafactory.metafactory |
string + | java 9 (JEP 280) | StringConcatFactory.makeConcatWithConstants |
records' toString, equals, hashCode | java 16 | ObjectMethods.bootstrap |
pattern switch | java 21 | SwitchBootstraps.typeSwitch |
switch on an enum with patterns | java 21 | SwitchBootstraps.enumSwitch |
doing the JVM's job by hand
a bootstrap method is a plain static method. nothing stops you calling one yourself, with the same arguments the JVM would pass, and looking at what comes back:
import java.lang.invoke.*;
void main() throws Throwable {
// what the JVM does the first time it hits: "hi " + name + " #" + n
CallSite site = StringConcatFactory.makeConcatWithConstants(
MethodHandles.lookup(),
"makeConcatWithConstants",
MethodType.methodType(String.class, String.class, int.class),
"hi \u0001 #\u0001");
MethodHandle target = site.getTarget();
IO.println(site.getClass().getSimpleName());
IO.println(target.type());
IO.println((String) target.invokeExact("ada", 7));
}ConstantCallSite (String,int)String hi ada #7
this one needs a real JVM (reflection), so it doesn't run in the browser. copy it into java Main.java to try it.
that's the whole protocol: a Lookup (so the bootstrap acts with your class's access rights), the name and type from the instruction, then the static arguments. out comes a CallSite whose target has exactly the type of the call site, so the JVM can call it with no adaptation.
write your own call site
javac never lets you write invokedynamic yourself. but the Class-File API does. here's a class, generated at runtime, whose method is one invokedynamic pointing at a bootstrap we wrote:
import java.lang.classfile.ClassFile;
import java.lang.constant.*;
import java.lang.invoke.*;
import static java.lang.constant.ConstantDescs.*;
static int boots = 0;
// the bootstrap method. the JVM calls this once per call site.
static CallSite bsm(MethodHandles.Lookup caller, String name, MethodType type) {
boots++;
IO.println("bootstrap: linking '" + name + "' as " + type);
return new ConstantCallSite(MethodHandles.constant(String.class, "hello from a call site"));
}
void main() throws Throwable {
var bootstrap = MethodHandleDesc.ofMethod(DirectMethodHandleDesc.Kind.STATIC,
ClassDesc.of("Main"), "bsm",
MethodTypeDesc.of(CD_CallSite, CD_MethodHandles_Lookup, CD_String, CD_MethodType));
var greet = DynamicCallSiteDesc.of(bootstrap, "greet", MethodTypeDesc.of(CD_String));
byte[] bytes = ClassFile.of().build(ClassDesc.of("Hello"), cls -> cls
.withMethodBody("run", MethodTypeDesc.of(CD_String), ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC,
code -> code.invokedynamic(greet).areturn()));
Class<?> hello = MethodHandles.lookup().defineClass(bytes);
MethodHandle run = MethodHandles.lookup().findStatic(hello, "run", MethodType.methodType(String.class));
for (int i = 0; i < 3; i++) IO.println((String) run.invokeExact());
IO.println("bootstrap ran " + boots + " time(s)");
}bootstrap: linking 'greet' as ()String hello from a call site hello from a call site hello from a call site bootstrap ran 1 time(s)
this one needs a real JVM (reflection), so it doesn't run in the browser. copy it into java Main.java to try it.
three calls, one bootstrap. the instruction asked for a call named greet of type ()String; our bootstrap answered with a constant handle, and from then on Hello.run() just returns the string.
what linking costs
the price of deciding at runtime is paid at runtime, once per call site. i timed first executions with System.nanoTime() in fresh JVMs (jdk 25, apple M4 Pro, five runs each, typical values):
a couple of milliseconds, once. it matters for startup (a big app links thousands of sites), and it's one of the things CDS and the AOT cache chip away at. it doesn't matter at all for throughput: once linked and JIT-compiled, a lambda call is as fast as a call through an anonymous class, and a concatenation is usually faster than the StringBuilder code javac used to write.
call sites that change
a ConstantCallSite is linked forever. that's what java's own features use, and it's what lets the JIT treat the target as a constant. but there are two more kinds, MutableCallSite and VolatileCallSite, whose target can be swapped at any time:
import java.lang.invoke.*;
import static java.lang.invoke.MethodType.methodType;
void main() throws Throwable {
var lookup = MethodHandles.lookup();
MethodHandle upper = lookup.findVirtual(String.class, "toUpperCase", methodType(String.class));
MethodHandle strip = lookup.findVirtual(String.class, "strip", methodType(String.class));
MutableCallSite site = new MutableCallSite(methodType(String.class, String.class));
MethodHandle call = site.dynamicInvoker(); // what an indy instruction would be bound to
site.setTarget(upper);
IO.println("[" + (String) call.invokeExact(" ada ") + "]");
site.setTarget(strip); // relink: same call site, new behaviour
IO.println("[" + (String) call.invokeExact(" ada ") + "]");
}[ ADA ] [ada]
this one needs a real JVM (reflection), so it doesn't run in the browser. copy it into java Main.java to try it.
this is what invokedynamic was originally built for. in a dynamic language, a.foo() can't be resolved at compile time: it depends on what a turns out to be. JRuby's bootstrap returns a mutable call site whose target is a guard: "if the receiver is the class i saw last time, call that method directly, else relink". that's an inline cache, the same trick HotSpot uses internally for virtual calls (JIT optimizations), built out of method handle combinators. when the target changes, the JIT throws away code that inlined the old one and recompiles.
the constant-pool cousin: dynamic constants+
java 11 added the same idea for constants: a CONSTANT_Dynamic entry in the constant pool (JEP 309), "condy" for short. instead of a literal, the entry names a bootstrap method; the first ldc of that entry calls the bootstrap and caches whatever it returns as the constant, forever. it's a lazily computed static final that doesn't need a class initializer. javac uses it in at least one place you can see: put enum constants as case labels in a pattern switch over a sealed interface, and javap -v shows entries like Dynamic #2:invoke:Ljava/lang/Enum$EnumDesc;, whose bootstrap is ConstantBootstraps.invoke calling EnumDesc.of. those constants are then passed as static arguments to SwitchBootstraps.typeSwitch. the Class-File API can emit them with DynamicConstantDesc. frameworks that generate bytecode use it to embed objects that can't be written as literals.
invokedynamicnames a bootstrap method, not a target.- first execution: the JVM calls the bootstrap, which returns a
CallSite. linked once, per instruction. - javac describes what; JDK code decides how, and can improve without recompiling you.
- lambdas, string
+, record methods and patternswitchall compile to it. - linking costs microseconds to milliseconds once. a linked site is as cheap as a direct call.