Week 7 - Method Handles, VarHandles, and Reflection¶
Conceptual Core¶
Three generations of "do something dynamically" in the JVM, each with a different cost model:
java.lang.reflect(1.1+) - original.Method.invokeis a native dispatcher, opaque to the JIT, ~10-100× slower than a direct call.MethodHandle(7+) - typed handles, JIT-inlinable. Substrate for lambdas, Spring 5+, Hibernate 6+, Jackson 2+.VarHandle(9+) - typed handles to variables with explicit memory-ordering modes. Replaces almost every legitimate use ofsun.misc.Unsafe.
The mental model: reflection for tooling, MethodHandle for hot-path dynamic dispatch, VarHandle for concurrent code. Pick wrong, hot path is 50× slower than necessary.
The trap
Caching a Method in a static field thinking it's "fast now." It isn't - every .invoke() still pays the dispatcher cost. The 7+ idiom: lookup.unreflect(method) to convert to a MethodHandle, cache that, call .invokeExact(args...).
Mechanical Detail¶
MethodHandles.Lookupis capability-based: it encodes the access rights of the class that obtained it.lookup.findVirtual(...),lookup.findStatic(...). To reach private members in another class, useMethodHandles.privateLookupIn(target, lookup)- modern replacement forsetAccessible(true).invokeExactvsinvoke:invokeExactrequires the call-site signature to exactly match the handle'sMethodType(including return type).invokeallows asType conversions but loses inlining. AlwaysinvokeExactin hot paths.VarHandlememory modes:Plain,Opaque,Acquire/Release,Volatile- map 1:1 to C++20relaxed/opaque/acquire+release/seq_cst. Use the weakest that proves correctness.- Foreign Function & Memory API (
java.lang.foreign, stable in 22+) -MemorySegment,MemoryLayout,Linker.nativeLinker()returns aMethodHandleto a C function. Replaces JNI for new code.MethodHandleis the substrate. - The strong-encapsulation saga: Java 9 introduced modules; 17 made
--illegal-access=denypermanent. Old libraries reaching intojava.baseneed--add-opens java.base/java.lang=ALL-UNNAMED. Fix path: prefer Panama/VarHandle/privateLookupInover--add-opens.
Lab¶
Build a tiny DI container (~150 lines): scan a package for @Inject constructors, topologically sort by dependencies, instantiate with MethodHandle (lookup.unreflectConstructor(ctor).invokeExact(deps)). JMH vs Constructor.newInstance(deps). Expect MethodHandle ~5-10× faster after warmup. Stretch: use LambdaMetafactory to get within 1.5× of a direct call.
Idiomatic Drill¶
Find every setAccessible(true) in a codebase you maintain. Classify: legitimately needed (→ MethodHandles.privateLookupIn), replaceable with a public API, or a leak waiting to happen.
Production Hardening Slice¶
--add-opens java.base/java.lang=ALL-UNNAMED # only for known legacy
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintModuleResolution
--add-opens requirement, the diff tells you which package needs review.