docs/architecture/ExtensionInvokeDynamicBridge.md
The BTrace extension system uses invokedynamic to bridge between script classloaders and extension classloaders, avoiding bootstrap classloader pollution while maintaining complete extension isolation.
Key Benefits:
┌──────────────────────────────────────┐
│ BTrace Script │
│ ┌──────────────────────────────────┐ │
│ │ @Injected MetricsService metrics │ │
│ │ │ │
│ │ void onMethod() { │ │
│ │ metrics.histogram("foo") ←────┼─┼── INVOKEDYNAMIC call
│ │ } │ │
│ └──────────────────────────────────┘ │
│ │
│ Classloader: Anonymous (parent=null)│
└────────────┬─────────────────────────┘
│ delegates to bootstrap
↓
┌──────────────────────────────────────┐ ┌────────────────────────────┐
│ Bootstrap ClassLoader │ │ Extension ClassLoaders │
│ │ │ │
│ ┌─────────────────────────────────┐ │ │ ┌────────────────────────┐ │
│ │ ExtensionIndy.java │ │◄────────┼─┤ btrace-metrics │ │
│ │ │ │ bridge │ │ - MetricsService.class │ │
│ │ public static CallSite │ │ │ │ - HdrHistogram (shaded)│ │
│ │ bootstrapFieldGet(...) { │ │ │ └────────────────────────┘ │
│ │ // Get extension class │ │ │ │
│ │ Class<?> clz = │ │ │ ┌────────────────────────┐ │
│ │ bridge.getExtensionClass(); │ │ │ │ btrace-statsd │ │
│ │ // Create MethodHandle │ │ │ │ - (isolated) │ │
│ │ return new ConstantCallSite(mh)│ │ │ └────────────────────────┘ │
│ │ } │ │ │ │
│ └─────────────────────────────────┘ │ │ ┌────────────────────────┐ │
│ │ │ │ custom-extension │ │
│ ┌─────────────────────────────────┐ │ │ │ - (isolated) │ │
│ │ ExtensionBridge (interface) │ │ │ └────────────────────────┘ │
│ │ - getExtensionClass() │ │ │ │
│ └─────────────────────────────────┘ │ └────────────────────────────┘
│ │ ▲
│ ┌─────────────────────────────────┐ │ │
│ │ btrace-boot.jar │ │ │
│ │ - BTraceRuntime │ │ │
│ │ - BTraceUtils │ │ │
│ │ - Core annotations │ │ │
│ └─────────────────────────────────┘ │ │
└──────────────────────────────────────┘ │
▲ │
│ implements │
│ │
┌────────────┴─────────────────────┐ │
│ btrace-agent │ │
│ │ │
│ ┌──────────────────────────────┐ │ │
│ │ ExtensionBridgeImpl │ │ │
│ │ │ │ │
│ │ static { │ │ │
│ │ // Set ExtensionIndy.bridge│ │ │
│ │ ExtensionIndy.bridge = │ │ │
│ │ new ExtensionBridgeImpl()│ │ │
│ │ } │ │ │
│ │ │ │ │
│ │ Class<?> getExtensionClass() │ │ │
│ │ // Find extension │ │ │
│ │ // Load extension │─┼──────────────────────────┘
│ │ // Return class from │ │
│ │ // extension CL │ │
│ └──────────────────────────────┘ │
│ │
│ ┌──────────────────────────────┐ │
│ │ ExtensionLoader │ │
│ │ - discoverExtensions() │ │
│ │ - load(ExtensionDescriptor) │ │
│ │ - findExtensionForService() │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────┘
Location: btrace-runtime/src/main/java/org/openjdk/btrace/runtime/ExtensionIndy.java
Purpose: Bootstrap methods for invokedynamic extension access
Key Methods:
public static CallSite bootstrapFieldGet(
MethodHandles.Lookup caller,
String fieldName,
MethodType type,
String serviceClassName,
String serviceType,
String factoryMethod) throws Exception
Responsibilities:
Error Handling:
Location: btrace-runtime/src/main/java/org/openjdk/btrace/runtime/ExtensionBridge.java
Purpose: Interface for agent-to-runtime classloader bridging
API:
public interface ExtensionBridge {
Class<?> getExtensionClass(String serviceClassName) throws Exception;
}
Why an Interface:
Location: btrace-agent/src/main/java/org/openjdk/btrace/agent/extension/ExtensionBridgeImpl.java
Purpose: Agent-side implementation of ExtensionBridge
Initialization:
static {
try {
Class<?> indyClz = Class.forName("org.openjdk.btrace.runtime.ExtensionIndy");
ExtensionBridge bridge = new ExtensionBridgeImpl(Main.getExtensionLoader());
indyClz.getField("bridge").set(null, bridge);
} catch (ClassNotFoundException e) {
// Expected for pre-Java 8 or if ExtensionIndy unavailable
}
}
Responsibilities:
ExtensionIndy.bridge field during static initializationLocation: btrace-instr/src/main/java/org/openjdk/btrace/instr/Preprocessor.java
Purpose: Transforms @Injected field references to invokedynamic
Transformation:
// Before:
GETSTATIC ScriptClass.metrics : MetricsService
// After:
INVOKEDYNAMIC bootstrapFieldGet(
"metrics", // field name (debug)
"()Lorg/openjdk/btrace/metrics/MetricsService;", // method descriptor
ExtensionIndy.bootstrapFieldGet, // bootstrap method
"org.openjdk.btrace.metrics.MetricsService", // service class name
"RUNTIME", // optional type hint (deprecated)
"" // factory method (empty = constructor)
) : MetricsService
Modified Method:
private AbstractInsnNode updateInjectedUsage(
ClassNode cn, FieldInsnNode fin, InsnList l, LocalVarGenerator lvg)
Changes:
Location: btrace-agent/src/main/java/org/openjdk/btrace/agent/extension/ExtensionLoader.java
Key Changes:
instrumentation.appendToBootstrapClassLoaderSearch(jarFile)Impact:
1. Main.agentmain/premain()
↓
2. ExtensionLoader.discoverExtensions()
- Scans extensions/ directory
- Creates ExtensionDescriptor for each extension
- Stores in availableExtensions map
- Does NOT load extensions yet
↓
3. ExtensionBridgeImpl.<clinit>() (static initializer)
- Creates new ExtensionBridgeImpl instance
- Sets ExtensionIndy.bridge = instance
- Bridge now ready for invokedynamic bootstrap calls
↓
4. Scripts can now be loaded and compiled
1. Script bytecode contains:
INVOKEDYNAMIC bootstrapFieldGet(...) : MetricsService
↓
2. JVM calls ExtensionIndy.bootstrapFieldGet()
(first time for this callsite)
↓
3. ExtensionIndy.bootstrapFieldGet():
a. Call bridge.getExtensionClass("org.openjdk.btrace.metrics.MetricsService")
↓
4. ExtensionBridgeImpl.getExtensionClass():
a. Call loader.findExtensionForService(serviceClassName)
b. If ext.isLoaded() == false:
- loader.load(ext) // Loads extension JAR into ExtensionClassLoader
c. extClassLoader.loadClass(serviceClassName)
d. Return Class<?> to bootstrap method
↓
5. ExtensionIndy.bootstrapFieldGet() (continued):
a. Create MethodHandle for service instantiation:
- Auto-detect constructor or factory
- Initialize extensions via Extension.initialize(ExtensionContext) when first accessed
b. Return new ConstantCallSite(methodHandle)
↓
6. JVM caches ConstantCallSite
↓
7. Subsequent calls to same INVOKEDYNAMIC:
- Use cached CallSite (zero overhead)
- MethodHandle directly instantiates service
- No bootstrap method re-execution
1. Script executes: metrics.histogram("foo")
↓
2. INVOKEDYNAMIC instruction
↓
3. JVM uses cached CallSite (no bootstrap)
↓
4. MethodHandle executes:
- Instantiates MetricsService
- Returns service instance
↓
5. INVOKEVIRTUAL MetricsService.histogram(String)
- Normal virtual dispatch
- JVM resolves from actual object class
- Extension method executes
Overhead:
One-time cost per @Injected field per script
Overhead: Zero (compared to direct method call)
Before (Bootstrap Classpath):
After (invokedynamic Bridge):
Preferred pattern: extensions expose a public no-arg constructor and receive
their runtime context via Extension.initialize(ExtensionContext).
Constructor Example:
public class MyExtension extends Extension {
public MyExtension() {
// No-arg constructor
}
}
MethodHandle Creation:
MethodHandle constructor =
MethodHandles.publicLookup().findConstructor(
serviceClass,
MethodType.methodType(void.class));
Factory methods are also supported but less common in the new model.
MethodHandle Creation:
MethodHandle factory =
MethodHandles.publicLookup().findStatic(
serviceClass,
"getInstance",
MethodType.methodType(serviceClass));
If an extension needs access to runtime context, override initialize(ExtensionContext)
in your subclass of Extension. The bridge calls it after construction.
Initialization Example:
public class MyExtension extends Extension {
@Override
public void initialize(ExtensionContext ctx) {
super.initialize(ctx);
// Use ctx as needed
}
}
MethodHandle Creation:
Class<?> runtimeImplClass =
Class.forName("org.openjdk.btrace.runtime.BTraceRuntime$Impl");
MethodHandle constructor =
MethodHandles.publicLookup().findConstructor(
serviceClass,
MethodType.methodType(void.class, runtimeImplClass));
// Bind BTraceRuntime.enter() as first argument
MethodHandle getRuntimeImpl =
MethodHandles.publicLookup().findStatic(
BTraceRuntime.class,
"enter",
MethodType.methodType(runtimeImplClass));
mh = MethodHandles.filterReturnValue(getRuntimeImpl, constructor);
// In BTraceRuntimeImpl_8.defineClass()
ClassLoader loader = new ClassLoader(null) {}; // parent = null
Class<?> cl = unsafe.defineClass(name, code, 0, code.length, loader, null);
Parent: null (delegates directly to bootstrap) Visibility: Only bootstrap classes Impact: Scripts can only see classes in bootstrap classpath
// In ExtensionLoader.load()
ExtensionClassLoader classLoader = new ExtensionClassLoader(
extensionId,
extensionVersion,
new URL[] {jarUrl},
parentClassLoader // BTrace boot classloader
);
Parent: BTrace boot classloader (btrace-boot.jar) Visibility: Extension JAR + BTrace core APIs Isolation: Each extension in separate classloader
Shaded Dependencies:
Script Class
├── parent = null → Bootstrap ClassLoader
│ ├── JRE system classes
│ ├── btrace-boot.jar (BTraceRuntime, annotations)
│ └── ExtensionIndy.class
│
ExtensionClassLoader (per extension)
├── parent = BTrace Boot ClassLoader
│ └── btrace-boot.jar classes
├── Extension JAR classes
└── Shaded dependencies
invokedynamic bridges the gap:
invokedynamic features used:
INVOKEDYNAMIC instruction (Java 7+)MethodHandles (Java 7+)CallSite / ConstantCallSite (Java 7+)Handle (Java 7+)NOT used:
Lookup.defineHiddenClass) - Java 15+ onlyResult: Works on all BTrace-supported Java versions (8+)
Indy.java (probe handlers):
ExtensionIndy.java (extensions):
Causes:
Behavior:
try {
// Load class and create MethodHandle
} catch (Throwable t) {
// Graceful degradation: return null
mh = MethodHandles.constant(type.returnType(), null);
}
return new ConstantCallSite(mh);
Impact:
Enable logging:
-Dorg.slf4j.simpleLogger.log.org.openjdk.btrace.agent.extension=DEBUG
-Dorg.slf4j.simpleLogger.log.org.openjdk.btrace.runtime.ExtensionIndy=DEBUG
Logs show:
Bytecode inspection:
javap -v ScriptClass.class | grep -A 5 INVOKEDYNAMIC
Shows:
ExtensionLoader.load():
if (instrumentation != null) {
JarFile jarFile = new JarFile(descriptor.getJarPath().toFile());
instrumentation.appendToBootstrapClassLoaderSearch(jarFile);
}
Preprocessor.updateInjectedUsage():
// Create local variable
int varIdx = lvg.newVar(implType);
// Emit instantiation bytecode
toInsert.add(new TypeInsnNode(Opcodes.NEW, implType.getInternalName()));
toInsert.add(new InsnNode(Opcodes.DUP));
toInsert.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, ...));
// Store in local variable
toInsert.add(new VarInsnNode(Opcodes.ASTORE, varIdx));
ExtensionLoader.load():
// Extension classes accessed via invokedynamic bridge
// (no bootstrap classpath pollution)
Preprocessor.updateInjectedUsage():
// Emit invokedynamic instruction
InvokeDynamicInsnNode indyInsn = new InvokeDynamicInsnNode(
fieldName,
methodDescriptor,
bootstrapHandle,
serviceClassName,
serviceType,
factoryMethod);
Existing scripts: No changes required
Existing extensions: No changes required
Behavioral changes: None
Before:
After:
MethodHandles.publicLookup():
Lookup Context:
ExtensionIndy:
ExtensionBridgeImpl:
MetricsTest:
Verification:
# Check bytecode contains INVOKEDYNAMIC
javap -v integration-tests/build/classes/java/test/MetricsTest.class | grep INVOKEDYNAMIC
# Check extension NOT in bootstrap
jcmd <pid> VM.class_hierarchy | grep -v btrace-metrics
Concept:
btrace-metrics-api.jar → bootstrap (interfaces only)btrace-metrics-impl.jar → extension CL (implementations)Benefits:
Current: Each invokedynamic creates new instance Enhancement: Cache service instances per script
Implementation:
// In ExtensionIndy
private static final Map<String, Object> instanceCache = new ConcurrentHashMap<>();
// In bootstrapFieldGet()
String cacheKey = caller.lookupClass().getName() + ":" + serviceClassName;
Object cached = instanceCache.get(cacheKey);
if (cached != null) {
return new ConstantCallSite(MethodHandles.constant(type.returnType(), cached));
}
Challenge: Extension updates require agent restart Enhancement: Use MutableCallSite instead of ConstantCallSite
Implementation:
public static CallSite bootstrapFieldGet(...) {
MethodHandle mh = createServiceHandle(...);
return new MutableCallSite(mh); // Allows invalidation
}
// On extension reload:
public static void invalidateExtension(String extensionId) {
// Find all MutableCallSites for this extension
// Call MutableCallSite.setTarget() with new MethodHandle
}
The invokedynamic extension bridge provides:
✅ Clean bootstrap namespace - Only BTrace core in bootstrap ✅ Extension isolation - Each extension in own classloader ✅ Java 8+ compatible - No Java 15+ features required ✅ Zero performance overhead - After first call ✅ Backward compatible - No script changes required ✅ Better security - Extensions lack bootstrap privileges ✅ On-demand loading - Extensions loaded when needed
This architecture enables scalable, secure, and performant extension loading while maintaining full compatibility with existing BTrace scripts and extensions.