History of the question
The introduction of invokedynamic in Java 7 via JSR 292 brought the MethodHandle API to support dynamic language implementations on the JVM. The challenge was that MethodHandle.invoke needed to accept any combination of argument types and return types without declaring thousands of overloads. The JVM architects solved this by introducing the concept of polymorphic signature methods, marked internally by the @PolymorphicSignature annotation within the java.lang.invoke package.
The problem
Standard Java method invocation requires the compiler to emit an invokevirtual (or similar) instruction referencing a specific method descriptor in the constant pool that exactly matches the method's declared signature. If MethodHandle.invoke were declared to take Object... args, every call site would require boxing and array allocation, defeating performance goals. Conversely, declaring overloads for every possible signature combination is impossible and would bloat the Class file infinitely.
The solution
The JVM treats methods annotated with @PolymorphicSignature specially. When the compiler encounters a call to such a method, it ignores the declared signature and instead generates an invokevirtual instruction whose method descriptor exactly matches the erased types of the arguments and return type at the call site. This allows MethodHandle.invokeExact to appear as accepting (Object)Object in source code but compile to (String)int at a specific call site. The JVM then links this call directly to the target method's entry point without adapter overhead.
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; public class PolymorphicExample { public static void main(String[] args) throws Throwable { MethodHandle handle = MethodHandles.lookup() .findVirtual(String.class, "length", MethodType.methodType(int.class)); // The compiler generates invokevirtual with descriptor (String)int // despite invokeExact being declared as (Object)Object in bytecode int result = (int) handle.invokeExact("hello"); System.out.println(result); // Outputs: 5 } }
Problem description
While building a high-throughput event processing framework for financial tick data, we needed to dispatch incoming messages to registered handlers using reflection-like flexibility but with zero-allocation overhead. Each handler method had different signatures—some accepted long timestamps, others BigDecimal prices—making generic dispatch challenging without boxing primitives.
Different solutions considered
Dynamic bytecode generation involved using ASM or ByteBuddy to generate proxy classes for each handler signature at registration time. This approach offered near-native performance after warm-up but consumed significant Metaspace and increased application startup latency by several seconds during class loading and JIT compilation. It also added maintenance complexity for debugging generated code.
Reflection with method handles utilized standard Method.invoke followed by unreflect to obtain MethodHandles. While simpler to implement, this imposed boxing costs for primitive arguments and prevented HotSpot from inlining through the reflective layer. Performance testing showed 10-15x slower dispatch compared to direct calls, violating our latency requirements.
Polymorphic signature exploitation required carefully casting arguments to exact expected types before calling invokeExact. This allowed the compiler to generate signature-specific invokevirtual instructions for each call site, effectively treating the MethodHandle as a typed function pointer. The trade-off was compile-time type rigor—we had to validate handler signatures during registration to ensure type safety, and the code would not compile if signatures mismatched.
Chosen solution and why
We selected the polymorphic signature approach combined with a registration-time validation layer. By generating lightweight adapter lambdas (using LambdaMetafactory and invokedynamic) that matched exact MethodHandle signatures, we achieved direct-call performance while maintaining type safety. The JVM could inline through the MethodHandle to the actual handler method, eliminating dispatch overhead entirely.
Result
The system processed 2.5 million events per second with sub-microsecond latency, matching hand-written dispatch code performance. GC pressure dropped by 98% compared to the reflection-based prototype, as primitive arguments no longer required boxing during the invocation path. The solution remained maintainable because type errors were caught at compile time rather than runtime.
Why does MethodHandle.invoke() permit type conversion while invokeExact() requires precise signature matching despite both having polymorphic signatures?
Both methods carry the @PolymorphicSignature annotation, but invokeExact performs strict signature checking at the JVM level. When the compiler generates the invokevirtual instruction for invokeExact, it uses the exact erased types at the call site. The JVM then verifies that these types precisely match the target MethodType. In contrast, invoke (without Exact) includes logic to adapt the call site types to the target type using MethodHandle.asType adapters, which perform boxing, unboxing, and primitive conversions. This adaptation happens within the MethodHandle implementation rather than at the call site, making invoke more flexible but potentially slower due to adapter chain overhead.
How does the JVM prevent type safety violations if polymorphic signature methods allow arbitrary method descriptors?
The JVM relies on the Java compiler to enforce type safety at the source level. Because @PolymorphicSignature is restricted to java.base module classes (like MethodHandle and VarHandle), user code cannot declare new polymorphic methods. The compiler only permits polymorphic calls where it can verify the argument types against the expected signature at the call site. For invokeExact, the compiler inserts implicit casts to ensure the generated descriptor matches what the programmer intended. The JVM trusts that the compiler has performed this verification, allowing it to skip runtime descriptor checks during invocation, thereby achieving zero-overhead while maintaining safety through compile-time constraints.
Why do polymorphic signature methods appear to erase to Object types in stack traces and debugging, yet execute with specific primitive types?
The javac compiler emits the @PolymorphicSignature attribute in the class file for these methods. When the JVM resolves an invocation to such a method, it substitutes the descriptor from the call site's constant pool entry for the declared descriptor. This means the actual bytecode execution uses the specific types (int, long, etc.), but the method's metadata in the Class object retains the declared signature (typically (Object...)Object) for reflection purposes. Consequently, stack traces show the erased form because Throwable.fillInStackTrace uses the symbolic descriptor from the method's metadata, not the dynamic descriptor used during the actual invocation. This distinction confuses developers who expect to see the exact parameter types in debuggers.