Back to course

invokedynamic and Bootstraps

24 minadvancedModule 5: Linking and VersioningContinues from jvm-enums
24 min · Module 5: Linking and Versioning · Algorithms

Why This Matters

Every other instruction in this course names its target. invokestatic has a method reference; getfield has a field reference; new has a class. The target is written into the constant pool at compile time and looked up when the method runs.

invokedynamic does not. It carries a bootstrap method index, and the actual call target is computed by running that bootstrap method the first time the instruction executes. The class file does not say what is called. It says what to call to find out what to call.

This is the first point in the format where a class file is a program rather than a description. Everything before it was a layout the JVM could read and act on; invokedynamic makes the file contain instructions for constructing instructions. And it is the reason Java gained lambdas, method references, and — from Java 9 — string concatenation without changing the instruction set for either.

A Simple Model

Three pieces, and the middle one is the one that is not where a reader expects it.

1. the instruction, 5 bytes: u1 0xba invokedynamic u2 bootstrap_method_attr_index a CONSTANT_InvokeDynamic u2 0 two zero bytes, ALWAYS zero 2. the CONSTANT_InvokeDynamic entry, 4 bytes: u1 tag = 18 u2 bootstrap_method_attr_index -> into BootstrapMethods u2 name_and_type_index the METHOD NAME and descriptor 3. the BootstrapMethods attribute: u2 num_bootstrap_methods then that many: u2 bootstrap_method_ref ONE u2 -- a CONSTANT_MethodHandle u2 num_bootstrap_arguments u2 bootstrap_arguments[num_bootstrap_arguments]

Point 3's single u2 is the detail that costs an hour. Every other "reference to a method" in the format is a CONSTANT_Methodref — a class index and a name-and-type index, four bytes. A bootstrap method reference is one index, because it points at a CONSTANT_MethodHandle rather than a CONSTANT_Methodref. Reading two u2s there desynchronises the whole attribute immediately, and the resulting garbage looks structurally plausible for several entries before it fails.

This course's own decoder made exactly that mistake. It read method_ref and then read a second u2 as a name-and-type, and the result was a "number of arguments" of 134 in an 84-byte attribute — a value so obviously wrong that it was evidence, and the only reason it was noticed is that the attribute had to consume exactly 84 bytes.

And point 2 explains the division of labour. The NameAndType says what the call site is called and what signature it has; the bootstrap method says how to compute the target. For a lambda named get returning Supplier, the name and type are known at compile time and are what makes the call site's stack behaviour checkable. Everything else about the target — which class implements it, whether a synthetic class is generated at run time — is the bootstrap's problem.

The Actual Detail

All 84 bytes of the BootstrapMethods attribute from Lambda.class, decoded. Nine entries, and this parse agrees with javap on all nine and consumes exactly 84 bytes:

  [0] #174  3 args: #134 #135 #138     LambdaMetafactory
  [1] #174  3 args: #139 #140 #143     LambdaMetafactory
  [2] #174  3 args: #144 #145 #148     LambdaMetafactory
  [3] #174  3 args: #149 #150 #149     LambdaMetafactory
  [4] #174  3 args: #134 #153 #157     LambdaMetafactory
  [5] #174  3 args: #159 #160 #165     LambdaMetafactory
  [6] #181  1 args: #167               StringConcatFactory
  [7] #174  3 args: #134 #169 #138     LambdaMetafactory
  [8] #181  1 args: #172               StringConcatFactory

  0000: 00 09 00 ae 00 03 00 86 00 87 00 8a 00 ae 00 03
  0010: 00 8b 00 8c 00 8f 00 ae 00 03 00 90 00 91 00 94
  0020: 00 ae 00 03 00 95 00 96 00 95 00 ae 00 03 00 86
  0030: 00 99 00 9d 00 ae 00 03 00 9f 00 a0 00 a5 00 b5
  0040: 00 01 00 a7 00 ae 00 03 00 86 00 a9 00 8a 00 b5
  0050: 00 01 00 ac

Two different bootstrap methods in one file, and that is the second finding. Entries 0–5 and 7 use #174 = LambdaMetafactory.metafactory; entries 6 and 8 use #181 = StringConcatFactory.makeConcatWithConstants. The same instruction, two entirely different mechanisms, chosen per call site. The class file does not say "this is a lambda" — it says "compute the target with this method, given these arguments", and what that means is the bootstrap's business.

The three arguments, and what each one is for

Take entry 0, which backs #0:get:()Ljava/util/function/Supplier;:

  #134  MethodType   ()Ljava/lang/Object;        the ERASED signature
  #135  MethodHandle REF_invokeStatic Lambda.lambda$new$0:()Ljava/lang/String;
                                            the IMPLEMENTATION
  #138  MethodType   ()Ljava/lang/String;    the INSTANTIATED signature

  and the bootstrap method is called with three MORE parameters,
  which the file does not store because they come from the call site:

  (MethodHandles$Lookup, String name, MethodType type, ...)

Three stored arguments plus four implicit ones, and the implicit ones are why the machinery is larger than the lambda. The Lookup carries the calling class's access rights, the String is the name from the NameAndType, and the first MethodType is the call site's descriptor. So the bootstrap receives the call site's context as parameters and the file's contribution as arguments, and it combines them to return a CallSite that the JVM links.

The three stored ones are the parts the file genuinely has to record, and each has a job. The erased signature (#134) is what the call site was compiled against. The implementation handle (#135) points at the method that will actually run — a synthetic lambda$new$0 in this case, which is a real method in the file with real code. The instantiated signature (#138) is what generic adaptation produced: the same method, reachable with a different type. That is generic erasure being fixed up at load time rather than at compile time, and it is why Supplier<String> can be implemented by a ()Object method.

Entry 3 is the one that breaks the "javac generates a method" story. Its arguments are #149 #150 #149 — erased type, REF_invokeStatic Lambda.hello:()V, same type again. hello is an existing method in the class, not a generated one. The source was a method reference like Runnable r = this::hello;, and javac pointed the bootstrap straight at the existing method rather than generating a trampoline. A method reference to an existing compatible method costs no new code at all; only a lambda whose body needs the arguments adapted gets a synthetic lambda$new$N. The naming scheme from the inner-class concept reappears here — lambda$new$0 is a $ in a method name that has nothing to do with nesting, and the $new$ infix is javac's marker for "generated for an object-creation lambda".

Entries 6 and 8: one argument, and a recipe

The two StringConcatFactory entries have one argument where the lambdas have three, and that single argument is a recipe string:

  #7  InvokeDynamic #6:#53
       makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
                                  +-- the DESCRIPTOR of the concatenation
  [6] #181  1 args: #167         #167 is the recipe Utf8

  [8] #181  1 args: #172         a 4-argument recipe for
       makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;
                           Ljava/lang/String;Ljava/lang/String;Z)Ljava/lang/String;

The recipe is a template with holes — literal text interleaved with references to the arguments, which is why a two-string concatenation has one argument and a four-string-plus-boolean has one. The descriptor tells the bootstrap how many values to expect and their types; the recipe tells it what to do with them. And this is the Java 9 string concatenation change: "a" + b + "c" no longer compiles to a StringBuilder chain, and the makeConcat bytecode from Java 8 does not appear in the file at all.

Two observations about that. The StringBuilder chain was always a fiction — the JIT has devirtualised and eliminated it for years, so the only cost was the class file size. Moving it to invokedynamic moved the recipe from bytecode the JVM had to interpret at JIT time into a constant the bootstrap interprets once. And it is the same mechanism serving a purpose that has nothing to do with lambdas, which is the strongest evidence that invokedynamic is a general linking facility rather than a lambda feature with a general name.

The instruction, and the method handle kinds

The instruction is five bytes and the last two are always zero:

   5: invokedynamic #7,  0    // InvokeDynamic #0:get:()Ljava/util/function/Supplier;
  14: invokedynamic #17, 0    // InvokeDynamic #1:apply:()Ljava/util/function/Function;
  23: invokedynamic #25, 0    // InvokeDynamic #2:apply:()Ljava/util/function/BiFunction;
  32: invokedynamic #32, 0    // InvokeDynamic #3:run:()Ljava/lang/Runnable;

Those two zero bytes are reserved and always zero, and they are the only part of the instruction whose purpose is not obvious from the name. They exist because a later revision of the instruction needed the space; the format's usual answer to "we might want this" is to reserve bits rather than to version the opcode.

And the method handle reference kinds in that attribute — a u1 on the CONSTANT_MethodHandle — cover nine access modes that no other instruction uses:

  1 REF_getField         6 REF_invokeStatic
  2 REF_getStatic        7 REF_invokeSpecial
  3 REF_putField         8 REF_newInvokeSpecial
  4 REF_putStatic        9 REF_invokeInterface

  seen here:
    6 REF_invokeStatic   Lambda.lambda$new$0     the lambda body
    6 REF_invokeStatic   java/lang/invoke/LambdaMetafactory.metafactory
    6 REF_invokeStatic   java/lang/invoke/StringConcatFactory.makeConcatWithConstants
    8 REF_newInvokeSpecial java/util/ArrayList."<init>"
    5 REF_invokeVirtual  java/lang/String.isEmpty

Method handles are a second, richer reference form than the method references the other instructions use — nine kinds instead of four, covering field access and construction as well as invocation. That is a deliberate widening: a method handle can name a field or a constructor, so a bootstrap method can be handed "the getter for this field" and use it, and LambdaMetafactory does exactly that when adapting an implementation.

A Real Example

What a lambda actually costs, which is the number nobody expects, and the reason the metafactory has that six-parameter signature:

ONE lambda, `() -> "hi"`, measured in Lambda.class: 1 the invokedynamic instruction 5 bytes 1 CONSTANT_InvokeDynamic 4 bytes 1 CONSTANT_NameAndType 4 bytes 1 the NameAndType's two Utf8 entries ~12 bytes 1 BootstrapMethods entry 10 bytes 3 arguments: 2 MethodTypes + 1 MethodHandle 3 CONSTANT_MethodType 12 bytes 1 CONSTANT_MethodHandle 4 bytes 1 NameAndType for the handle 4 bytes 1 Utf8 for the impl's descriptor ~10 bytes 1 the synthetic method lambda$new$0 ~20 bytes of CODE -- plus the class it is in, and at RUN TIME 1 a generated hidden class per lambda ~1 KB, generated --------- the source was 11 characters.

The mechanism is more than the feature, by roughly two orders of magnitude. That is not a criticism of the design — it is what buying late binding costs, and the alternative at the time was either no lambdas or generating a class per lambda at compile time, which would have made the file even larger and fixed the target at compile time. The trade is: pay at load time, once, and pay in bytes, in exchange for the target not being fixed until it runs.

And the CallSite is the part that makes the late binding actually work. A CallSite is a mutable object holding a target, and the instruction reads it at execution time. The first execution runs the bootstrap and installs a target; every later execution is a virtual call through the now-populated site, which the JIT can inline exactly as it would any monomorphic call site. So the expensive path runs once and the hot path is a direct call — the same "pay at load, keep the hot path direct" bargain the nest attributes in Module 3 made for private field access, and it is why invokedynamic is fast despite the machinery.

Now the honest cost, because it is real and it is why this feature has a reputation. Each distinct lambda call site gets its own generated class at run time, and that class is loaded into the same metaspace as any other. A program with a hundred thousand distinct lambdas pays a hundred thousand class loads, and the failure mode is OutOfMemoryError: Metaspace rather than an allocation failure. The JVM has mitigations — a hidden class is not retained by a class loader's strong reference set, so it can be collected when the call site becomes unreachable, and lambdas that do not capture anything are cached — but the shape of the cost is real and worth knowing before reaching for lambdas in a hot path.

What invokedynamic bought, in one list. Lambdas and method references with no new instruction and no new class format — Java 8. String concatenation via StringConcatFactory, retiring a bytecode pattern — Java 9. And a general facility: any language compiling to the JVM can now define its own runtime-generated constructs by writing a bootstrap method, without asking the JVM for a new opcode. Kotlin's inline reification, Scala's implicit macros, and Clojure's reify are all built on it. That is the design's real significance — the class file became extensible at the instruction level, twenty years after it was extensible at the attribute level.

Try It Yourself

$ javap -v -p out/Lambda.class
$ python3 courses/jvm/assets/samples/class_decode.py out/Lambda.class

Check Your Understanding

Without looking back: you are parsing the BootstrapMethods attribute and you read a bootstrap method reference as #174 followed by a second u2 of 0x0003, then read that second value as the argument count and start reading 134 arguments from an 84-byte attribute. What did you read wrong, and what is the general reason this particular field is one u2 when every other method reference in the format is two?

Apply It

You are writing a bytecode-level verifier. It checks that every invokedynamic instruction's bootstrap index is in range and that the NameAndType descriptor matches the values actually on the stack at that point. It passes every class in the JDK's own modules. A tool you run later on a third-party class reports "descriptor mismatch: expected one argument, found zero" on a class that loads and runs correctly on the same JVM. What is the check getting wrong, and what is the strongest evidence in the situation that it is the check and not the class?

The general habit: in a format where several fields describe the same construct, know which one is a promise the machine will enforce and which is a hint. Verify the promise; treat the hint as unverified until something independent confirms it.

Connect

A class file that computes its own instructions is the same idea as a relocation, and seeing the two side by side sharpens both. An ELF relocation says a symbol's address goes here and the loader fills it in; an invokedynamic says compute a target with this method and these arguments and the JVM links it on first execution. Both defer work past compile time, and both exist because the compiler cannot know the answer — a symbol's address for a relocation, a lambda's target for an invokedynamic. The difference is the amount of computation permitted: a relocation gets a number written into a word, and an invokedynamic gets arbitrary Java code run once. A relocation is a fixed-point operation; a bootstrap is a program.

The method handle is where this connects to a PE's load config and to COFF weak externals, because a handle is a capability: it names a target and the access kind permitted on it, and the holder of a handle cannot do more than the handle allows. That is the same property as control flow guard or a weak reference resolving to nothing — a grant, not a description, and the JDK's whole MethodHandles and VarHandle API is built on the idea that a reference can be narrowed to exactly the access it needs. Compare a CONSTANT_Methodref, which grants access to every public member of its class: a handle is the narrower of the two, and bootstrap methods receive handles precisely so the capability can be passed along.

The reserved two zero bytes are a format's oldest habit. ELF's e_shnum is zero to mean "the real count is in section zero's size field", and several PE optional header fields use zero as an escape to a nearby field. Every format reserves a sentinel when it is not sure what it will need, and every one of those sentinels has eventually been used — which is the strongest argument for reserving space in a format you cannot change, and the strongest argument against assuming a reserved field will stay reserved.

The attribute inventory from the escape hatch listed BootstrapMethods at four occurrences across 24 samples, and it is worth noticing that it is the one common attribute that is not inert. SourceFile can be skipped freely; BootstrapMethods cannot be skipped by anything that executes the class, because every invokedynamic in the file needs it. An attribute's safety to skip is not uniform across the format, and this is the attribute where skipping stops being free. The qualification to "skip what you do not understand" that the sealed types concept raised is the same one, from the other direction: the attributes that carry meaning a machine acts on are the ones a verifier cannot treat as opaque.

Next: modules, the last structural feature the format added and the one that finally used two constant pool tags that javap still labels Unknown — which makes it the natural place to end the structural tour before the version history puts the whole format in order.