Java Agent is a JAR with a premain() or agentmain() method declared in MANIFEST.MF — the JVM calls it before main(), handing you an Instrumentation instance
ClassFileTransformer.transform() intercepts raw class bytes at load time — return null to skip, return modified bytes to transform
ASM is the standard library for bytecode manipulation — use AdviceAdapter for clean method entry/exit hooks
Startup agents (-javaagent) register before main(); Attach API (VirtualMachine.loadAgent) injects into a running JVM
Performance cost: transformation time per class load + injected code overhead per method call — scope to the narrowest package prefix
Biggest mistake: not shading ASM — Spring's bundled ASM version causes NoSuchMethodError at class load
✦ Definition~90s read
What is Java Agent and Instrumentation?
Java Agent Instrumentation is the JVM's built-in mechanism for modifying bytecode at load time or retransforming already-loaded classes at runtime. It exists because production Java applications often need to inject cross-cutting concerns—APM tracing, profiling, security policies, or AOP—without touching source code or recompiling.
★
Imagine every Java class is a recipe card in a chef's kitchen.
The core contract is a premain (or agentmain for dynamic attach) method in a JAR with a MANIFEST.MF that declares Premain-Class and optionally Can-Retransform-Classes. When you specify -javaagent:youragent.jar on the command line, the JVM invokes your premain before main(), passing an Instrumentation instance that lets you register ClassFileTransformer instances.
These transformers receive raw byte arrays and return modified ones, giving you full control over every class the JVM loads—including system classes if you set the right manifest flags.
Under the hood, the JVM calls your transformer's transform() method for each class definition or retransformation. You receive the class name, the current class loader (or null for the bootstrap loader), a protection domain, and the raw bytes. You return new bytes or null to skip.
This is where the COMPUTE_FRAMES crash bites you: when you use ASM's ClassWriter.COMPUTE_FRAMES option, ASM tries to compute stack map frames by simulating execution. It needs to resolve class references to determine types on the operand stack—but if the class loader that loaded the class being transformed isn't available during the computation (e.g., because the transformer runs in a different class loader context or the class loader has been garbage collected), ASM throws a ClassNotFoundException or NullPointerException.
The fix is either to use COMPUTE_MAXS instead (which only computes max stack/local sizes, not full frames) or to manually provide frame information, which is why production agents like ByteBuddy default to COMPUTE_MAXS and avoid frame computation entirely.
In the ecosystem, you have three main bytecode libraries: ASM is the lowest-level, giving you direct event-driven parsing of class files—it's what the JVM itself uses internally, and it's the fastest but most error-prone. ByteBuddy wraps ASM with a fluent API and handles frame computation safely by default, making it the go-to for new agents (used by DataDog, New Relic, and OpenTelemetry).
Javassist offers a higher-level source-like API but is slower and less flexible for complex transformations. You should not use Java Agent instrumentation when you can solve the problem with compile-time weaving (AspectJ), runtime proxies (Spring AOP), or when you're targeting Java 9+ modules without proper module exports—the module system can block access to internal JDK classes that agents often need to transform.
Plain-English First
Imagine every Java class is a recipe card in a chef's kitchen. Normally, the chef follows each recipe exactly as written. A Java agent is like a head chef who intercepts every recipe card before it reaches the line cooks — they can scribble notes on it, change an ingredient, or add a timing step, without ever touching the original recipe book. The line cooks (the JVM) never even know the card was modified; they just cook what's in front of them. That's instrumentation: quietly editing the instructions before they're executed.
Production Java systems are black boxes. You deploy a JAR, traffic hits it, and when something goes wrong — a memory leak, a slow database call, an unexpected exception — you're often blind. APM tools like Datadog, New Relic, and Dynatrace somehow see inside every method call, every HTTP request, every SQL query, all without you touching a single line of your application code. The mechanism behind all of them is the Java Agent API, and understanding it puts you in rare company.
The Java agent mechanism, introduced in Java 5 via the java.lang.instrument package, solves a profound problem: how do you add cross-cutting behavior — logging, tracing, profiling, security checks — to a running JVM without modifying source code or recompiling? The answer is bytecode instrumentation. At class-load time, before the JVM hands a class to its execution engine, an agent intercepts the raw bytes and rewrites them. It's surgical, powerful, and completely transparent to the application.
By the end of this article you'll be able to write a working Java agent from scratch, attach it to any JVM process, use the Instrumentation API to transform class bytecode with ASM, understand the production implications including memory overhead and class retransformation limits, and walk into an interview knowing exactly how tools like JaCoCo and OpenTelemetry Java work under the hood.
How Java Agent Instrumentation Actually Rewrites Your Bytecode
Java agent instrumentation is the ability to modify class bytecode at load time via the java.lang.instrument API. An agent registers a ClassFileTransformer that intercepts every class definition before the JVM links it. The transformer receives the raw byte array and can return a modified version — adding methods, injecting fields, or altering control flow. This happens in the primordial class loader context, which is the root cause of many crashes.
Agents attach either at startup (-javaagent) or dynamically via the Attach API. The critical property: the transformer runs inside the system class loader, not the target class's loader. When your transformed code references a class that isn't visible to the system loader, the JVM throws NoClassDefFoundError or, worse, a ClassCircularityError during COMPUTE_FRAMES. The ASM library's COMPUTE_FRAMES option, which calculates stack map frames automatically, triggers class loading for supertypes and interfaces — and if that load fails, the transformation crashes the JVM.
Use agents when you need cross-cutting behavior without source changes: APM tools, profilers, security scanners, or AOP frameworks. The cost is fragility around class loading boundaries. In production, a single missing dependency in the agent's classpath can silently corrupt stack frames, leading to VerifyError at runtime — not at agent load time.
⚠ COMPUTE_FRAMES triggers class loading
ASM's COMPUTE_FRAMES resolves supertypes at transform time. If the supertype is not visible to the system class loader, the agent crashes — not the target class.
📊 Production Insight
A monitoring agent using COMPUTE_FRAMES crashed every deployment that included a custom ClassLoader loading classes from a non-standard repository.
Symptom: JVM exits with 'java.lang.NoClassDefFoundError: com/example/CustomSuperClass' during class loading of an unrelated application class.
Rule: Never use COMPUTE_FRAMES in production agents — precompute frames with COMPUTE_MAXS or provide explicit stack map frames.
🎯 Key Takeaway
Agent transformers run in the system class loader — any class reference must be visible there or you get a crash.
COMPUTE_FRAMES is the #1 cause of agent-induced VerifyErrors in production; precompute frames or use COMPUTE_MAXS.
Always test agent transformations against the actual class loading hierarchy of your target application.
thecodeforge.io
Java Agent Instrumentation
How the JVM Loads a Java Agent — premain, MANIFEST.MF, and the Attach API
A Java agent is a plain JAR file with one special contract: a class containing a static premain method (for startup-time agents) or an agentmain method (for runtime attach). The JVM calls premain before your application's own main method, handing it an Instrumentation instance — the single most powerful object in the Java platform. Through it you can query loaded classes, redefine them, and most critically, register a ClassFileTransformer that intercepts every class as it's loaded.
The JAR's MANIFEST.MF is the contract document. Without the Premain-Class attribute pointing to your agent class, the JVM refuses to honour the -javaagent flag. You can also declare Can-Redefine-Classes: true and Can-Retransform-Classes: true there — both must be opt-in because they carry real overhead and security implications.
The Attach API (com.sun.tools.attach) lets you inject an agent into an already running JVM process without restarting it. This is how profilers like async-profiler and VisualVM work. The target JVM's PID is all you need. One critical catch: since Java 9, self-attach (attaching to your own PID) requires -Djdk.attach.allowAttachSelf=true or it throws an IOException.
One subtle point: premain runs on the main thread before any application code. That means you can safely set up singletons or static state without worrying about race conditions. But if your agent initialisation fails, the JVM will not start your application — it exits with an error. Make sure premain is idempotent and fast. Also note that the agent's own classes are loaded by the system classloader, not the bootstrap loader. If your agent needs to add classes to the bootstrap classpath (e.g., a Java agent that provides a new implementation of a core API), use instrumentation.appendToBootstrapClassLoaderSearch() — but this is rarely needed and carries its own risks.
TimingAgent.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.instrument.Instrumentation;
/**
* Entry point for a startup-time Java agent.
*
* Compilethisclass, package it in a JAR, and declare it in MANIFEST.MF:
* Premain-Class: TimingAgent
* Can-Retransform-Classes: true
*
* Launch with:
* java -javaagent:timing-agent.jar -jar your-app.jar
*/
public class TimingAgent {\n\n /**\n * TheJVM calls this BEFORE your application's main() method.\n *\n * @param agentArgs Any string args passed after '=' in -javaagent:jar=args\n * @param instrumentation The gateway to all bytecode transformation power\n */\n public static void premain(String agentArgs, Instrumentation instrumentation) {\n System.out.println(\"[TimingAgent] Agent loaded. JVM classes currently loaded: \"\n + instrumentation.getAllLoadedClasses().length);\n\n // Register our transformer — it will be called for EVERY class the JVM loads\n // from this point forward, including JDK classes if we don't filter carefully\n MethodTimingTransformer transformer = new MethodTimingTransformer(agentArgs);\n instrumentation.addTransformer(transformer, /* canRetransform= */ true);\n\n System.out.println(\"[TimingAgent] ClassFileTransformer registered. Ready.\");\n }\n\n /**\n * Called when the agent is attached to a RUNNING JVM via the Attach API.\n * The signature is identical to premain but the JVM lifecycle moment differs.\n */\n public static void agentmain(String agentArgs, Instrumentation instrumentation) {\n // Reuse the same startup logic — safe because we're stateless here\n premain(agentArgs, instrumentation);\n }\n}","output": "[TimingAgent] Agent loaded. JVM classes currently loaded: 412\n[TimingAgent] ClassFileTransformer registered. Ready."
}
Manifest File Setup Guide: Required Attributes and Common Mistakes
The META-INF/MANIFEST.MF file inside your agent JAR is the contract between you and the JVM. Without the correct attributes, the JVM will silently ignore your agent. The three essential attributes for a startup agent are: - Premain-Class: Fully qualified name of the class containing the premain(String, Instrumentation) method. - Agent-Class: Fully qualified name of the class containing the agentmain(String, Instrumentation) method (required for runtime attach). - Can-Retransform-Classes: Set to "true" if you plan to call retransformClasses() on already-loaded classes (required for most runtime attach scenarios). Optional but often needed: Can-Redefine-Classes, Boot-Class-Path.
The manifest file is whitespace-sensitive. Each line must end with a newline character, including the last line. Attribute names and values are separated by a colon and a space. No trailing spaces allowed. If an attribute value is too long, you can wrap it to the next line by starting the continuation line with a single space.
To generate the manifest automatically in Maven, use the maven-jar-plugin or maven-shade-plugin's ManifestResourceTransformer (as shown in the pom.xml example later). For manual builds, create a text file named manifest.txt with these contents:
A missing newline at the end of the manifest file causes the JVM to ignore the last attribute. Always verify with unzip -p agent.jar META-INF/MANIFEST.MF and check the output ends with a newline.
📊 Production Insight
A missing newline at end of MANIFEST.MF silently skips the Premain-Class attribute. The JVM logs nothing — the agent just never loads. Rule: always end MANIFEST.MF with a blank line. In Maven, ensure the ManifestResourceTransformer produces a proper final newline.
🎯 Key Takeaway
MANIFEST.MF must end with a newline. Use unzip -p to verify before shipping.
thecodeforge.io
Java Agent Instrumentation
Bytecode Manipulation Libraries: ASM vs ByteBuddy vs Javassist Compared
While ASM is the de facto standard for production Java agents, several libraries simplify bytecode manipulation with higher-level APIs. Choosing the right one depends on your performance requirements, complexity of transformations, and team expertise.
ASM (Low-level) – Direct bytecode instruction manipulation. Offers maximum performance and control. Used by the JDK itself. Requires understanding of the JVM instruction set and stack frames. Best for production APM agents where overhead must be minimal.
ByteBuddy (High-level) – Builds on ASM but provides a fluent API for common tasks like method delegation, field access, and type creation. Automatically handles frame computation and class writer configuration. Excellent for ad-hoc instrumentation and debugging tools. Some overhead compared to raw ASM, but negligible for typical use.
Javassist (Medium-level) – Uses a source-level API: you write Java code as strings that get compiled and injected. Very easy to get started, but limited for complex transformations (e.g., adding try-catch blocks). Performance is lower due to interpretation. Best for quick prototypes or simple method wrapping.
Feature
ASM
ByteBuddy
Javassist
API Level
Instruction-level
High-level (fluent)
Source-level (String)
Learning Curve
Steep
Moderate
Shallow
Performance
Fastest
Very fast (on ASM)
Moderate
Stack Frame Handling
Manual or COMPUTE_FRAMES
Automatic
Automatic
Class Creation
Possible but manual
Built-in (DynamicType)
Easy (new ClassPool)
File Size (jar)
~500 KB
~1.5 MB
~800 KB
Production Use
JaCoCo, OpenTelemetry
Mockito, Hibernate
Spring AOP (CGLib replacement)
For most production scenarios, ASM remains the best choice due to its performance and no-framework dependency. ByteBuddy is ideal when you need to write an agent quickly without deep bytecode expertise. Javassist is useful for simple filtering but not recommended for production agents that modify control flow.
ByteBuddyAgent.javaJAVA
1
2
3
4
5
6
7
8
9
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.instrument.Instrumentation;
publicclassByteBuddyAgent {
publicstaticvoidpremain(String args, Instrumentation inst) {\n newAgentBuilder.Default()\n .type(ElementMatchers.nameStartsWith(\"io.codeforge\"))\n .transform((builder, type, cl, mod) ->\n builder.method(ElementMatchers.any())\n .intercept(Advice.to(MethodTimer.class)))\n .installOn(inst);\n }\n\n public static class MethodTimer {\n @Advice.OnMethodEnter\n static long enter() { return System.nanoTime(); }\n @Advice.OnMethodExit\n static void exit(@Advice.Enter long start, @Advice.Origin String method) {\n System.out.println(method + \" took \" + (System.nanoTime() - start) + \" ns\");\n }\n }\n}",
"output": "// Build dependency: net.bytebuddy:byte-buddy-agent:1.14.12 + byte-buddy:1.14.12\n// Run with -javaagent:bytebuddy-agent.jar\n// Output: io.codeforge.demo.OrderService#processOrder took 4321871 ns"
}
Writing a ClassFileTransformer with ASM — Intercepting Method Calls in Raw Bytecode
The ClassFileTransformer interface has one method: transform. It receives raw class bytes and returns either modified bytes or null (meaning 'leave this class alone'). Returning null for classes you don't care about is critical for performance — every transformer in the chain gets called for every class, including java.lang.String and java.util.ArrayList.
To actually modify bytecode, you need a bytecode manipulation library. ASM is the industry standard — it's what the JDK itself uses internally. It operates at the instruction level at method entry, every exit point: a method can have multiple RETURN opcodes and any number of ATHROW instructions. ASM's AdviceAdapter handles this elegantly by providing onMethodEnter and onMethodExit hooks that account for all exit paths automatically, including the exception path.
The dependency you need is org.ow2.asm:asm:9.6 and org.ow2.asm:asm-commons:9.6. Bundle them into your agent JAR with Maven Shade or a fat-jar Gradle task.
AdviceAdapter works on all methods including constructors (<init>) and static initializers (<clinit>). Be careful with <init>: System.nanoTime() in a constructor of a frequently created object can add measurable overhead. Also, inside <clinit>, the class is being initialised, and calling System.nanoTime() is fine, but avoid any method calls that trigger additional class loading at that point — you can cause circular class loading deadlocks. If your transformer needs to skip <clinit> because of this, check the method name and return the original visitor for it.
MethodTimingTransformer.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
/**
* Interceptsclass loading and injects timing code into every method
* of classes whose package matches our target prefix.
*
* ASM version: 9.6 (org.ow2.asm:asm-commons:9.6)
*/
publicclassMethodTimingTransformerimplementsClassFileTransformer {
// Only instrument classes under this package to avoid touching JDK internalsprivatefinalString targetPackagePrefix;
publicMethodTimingTransformer(String agentArgs) {
// agentArgs might be something like "io/codeforge/demo"this.targetPackagePrefix = (agentArgs != null && !agentArgs.isBlank())
? agentArgs.replace('.', '/')
: "io/codeforge"; // default to our own package
}
@Overridepublicbyte[] transform(
ClassLoader loader,
String className, // Internally formatted: "com/example/MyClass"Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] originalClassBytes) {
// Fast-path: skip anything that's not our target to keep overhead minimalif (className == null || !className.startsWith(targetPackagePrefix)) {
return null; // null = "I didn't change anything, use original bytes"
}
try {\n ClassReader classReader = new ClassReader(originalClassBytes);\n // COMPUTE_FRAMES tells ASM to recalculate stack frames automatically\n // This is essential — manual frame calculation is error-prone\n ClassWriter classWriter = new ClassWriter(\n classReader, ClassWriter.COMPUTE_FRAMES);\n\n // Wrap the writer with our visitor that injects timing into each method\n TimingClassVisitor timingVisitor = new TimingClassVisitor(\n Opcodes.ASM9, classWriter, className);\n\n // EXPAND_FRAMES is required when using AdviceAdapter with COMPUTE_FRAMES\n classReader.accept(timingVisitor, ClassReader.EXPAND_FRAMES);\n\n System.out.println(\"[TimingAgent] Instrumented: \" + className);\n return classWriter.toByteArray(); // Return the rewritten bytecode\n\n } catch (Exception transformationError) {\n // NEVER let a transformer exception crash the JVM — log and return null\n System.err.println(\"[TimingAgent] Failed to instrument \" + className\n + \": \" + transformationError.getMessage());\n return null; // Fall back to the original unmodified class\n }\n }\n\n // ── Inner class: visits each class and delegates method visiting ─────────\n\n static class TimingClassVisitor extends ClassVisitor {\n\n private final String ownerClassName;\n\n TimingClassVisitor(int api, ClassVisitor delegate, String ownerClassName) {\n super(api, delegate);\n this.ownerClassName = ownerClassName;\n }\n\n @Override\n public MethodVisitor visitMethod(\n int access, String methodName, String descriptor,\n String signature, String[] exceptions) {\n\n // Get the default MethodVisitor from the ClassWriter chain\n MethodVisitor originalVisitor = super.visitMethod(\n access, methodName, descriptor, signature, exceptions);\n\n // Skip abstract and native methods — they have no bytecode body to visit\n boolean isAbstract = (access & Opcodes.ACC_ABSTRACT) != 0;\n boolean isNative = (access & Opcodes.ACC_NATIVE) != 0;\n if (isAbstract || isNative || originalVisitor == null) {\n return originalVisitor;\n }\n\n // Wrap with our timing advisor\n return new TimingMethodAdvisor(\n Opcodes.ASM9, originalVisitor, access,\n methodName, descriptor, ownerClassName);\n }\n }\n\n // ── Inner class: injects nanoTime calls at method entry and exit ──────────\n\n static class TimingMethodAdvisor extends AdviceAdapter {\n\n private final String ownerClass;\n private final String methodName;\n private int startTimeLocalVarIndex; // index of our injected local variable\n\n TimingMethodAdvisor(int api, MethodVisitor delegate, int access,\n String methodName, String descriptor, String ownerClass) {\n super(api, delegate, access, methodName, descriptor);\n this.ownerClass = ownerClass;\n this.methodName = methodName;\n }\n\n @Override\n protected void onMethodEnter() {\n // Inject: long __startTime = System.nanoTime();\n // INVOKESTATIC pushes the long result onto the stack\n mv.visitMethodInsn(\n Opcodes.INVOKESTATIC,\n \"java/lang/System\",\n \"nanoTime\",\n \"()J\",\n false);\n\n // Store that long into a new local variable\n // newLocal() allocates the next available slot in the local variable table\n startTimeLocalVarIndex = newLocal(Type.LONG_TYPE);\n mv.visitVarInsn(Opcodes.LSTORE, startTimeLocalVarIndex);\n }\n\n @Override\n protected void onMethodExit(int opcode) {\n // This is called for EVERY exit: RETURN, IRETURN, ARETURN, ATHROW, etc.\n // AdviceAdapter handles all the variants — we just implement this once\n\n // Inject: long __elapsed = System.nanoTime() - __startTime;\n mv.visitMethodInsn(\n Opcodes.INVOKESTATIC, \"java/lang/System\",\n \"nanoTime\", \"()J\", false);\n mv.visitVarInsn(Opcodes.LLOAD, startTimeLocalVarIndex);\n mv.visitInsn(Opcodes.LSUB); // stack now has elapsed nanoseconds (long)\n\n // Inject: TimingReporter.record(\"io/codeforge/Foo#doWork\", elapsed);\n // We push the method label as a constant string first\n mv.visitLdcInsn(ownerClass + \"#\" + methodName);\n // Swap so the stack order is (String label, long elapsed) matching the descriptor\n // Actually we need to restructure: push label first, then elapsed\n // Easiest: store elapsed, push label, reload elapsed\n int elapsedLocalVarIndex = newLocal(Type.LONG_TYPE);\n mv.visitVarInsn(Opcodes.LSTORE, elapsedLocalVarIndex);\n mv.visitLdcInsn(ownerClass + \"#\" + methodName);\n mv.visitVarInsn(Opcodes.LLOAD, elapsedLocalVarIndex);\n\n mv.visitMethodInsn(\n Opcodes.INVOKESTATIC,\n \"io/codeforge/agent/TimingReporter\",\n \"record\",\n \"(Ljava/lang/String;J)V\",\n false);\n }\n }\n}","output": "[TimingAgent] Instrumented: io/codeforge/demo/OrderService\n[TimingAgent] Instrumented: io/codeforge/demo/PaymentGateway\n[TimingAgent] Instrumented: io/codeforge/demo/InventoryRepository\n[TimingReporter] io/codeforge/demo/OrderService#processOrder => 4,312,871 ns (4.3 ms)\n[TimingReporter] io/codeforge/demo/PaymentGateway#charge => 231,048,203 ns (231 ms)\n[TimingReporter] io/codeforge/demo/InventoryRepository#findById => 892,441 ns (0.9 ms)"
}
Runtime Attach, Class Retransformation, and Production Performance Gotchas
Startup agents are clean but require a JVM restart. The Attach API lets you inject an agent into a live PID — indispensable for production diagnosis. The VirtualMachine.attach(pid) call establishes a socket connection to the target JVM's attach listener thread, then loadAgent sends the agent JAR path over that socket. The target JVM loads your JAR, calls agentmain, and if Can-Retransform-Classes: true is set, you can call instrumentation.retransformClasses(MyClass.class) to retroactively apply your transformer to already-loaded classes.
Retransformation is powerful but constrained. You cannot change the class schema: no adding fields, no adding methods, no changing method signatures or the class hierarchy. You can only change method bodies. Violating this causes an UnsupportedOperationException with the message 'class redefinition failed: attempted to change the schema'. This is a JVM limitation, not ASM's — the JVM's HotSpot runtime can hot-swap method bodies but cannot reorganize vtables or field layouts without a full GC pause and internal restructuring it simply doesn't support.
The performance cost of instrumentation has three parts. First, transformation time: every class load runs your transformer. Keep the fast-path null return tight — a single startsWith check costs nanoseconds. Second, the injected code itself: System.nanoTime() is not free (it's a JNI call on some platforms), and if you're logging inside transformed methods you're paying I/O costs on every call. Third, and most insidious: ClassWriter.COMPUTE_FRAMES triggers ASM to load referenced classes through the provided ClassLoader to compute frame types, which can cause unexpected class loading side effects. Pass the original ClassLoader from the transform signature into your ClassWriter to avoid ClassNotFoundException at transformation time.
The Attach API uses a socket file (/tmp/.java_pid<pid> on Linux). If the target JVM is running in a container without shared /tmp, the attach listener socket may not be accessible. This is a common issue in Kubernetes environments. To work around, ensure the /tmp directory is shared or use -XX:+StartAttachListener with a custom path via -XX:+AttachListener. Also, the attach listener thread might be blocked by a long GC pause — the attach() call will timeout after about 10 seconds. In such cases, retry with a longer timeout (unfortunately the JVM doesn't expose a configurable timeout; you'll need to implement a retry loop in your attacher).
LiveAttachDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
/**
* Attaches a Java agent to a RUNNINGJVM without restarting it.
*
* Requires: JDK (not just JRE), and the target JVM must allow self-attach or
* be a separate process. Runthis as a standalone program.
*
* Compile: javac --add-exports java.base/sun.nio.ch=ALL-UNNAMEDLiveAttachDemo.java
* Run: java -cp .:$JAVA_HOME/lib/tools.jar LiveAttachDemo <target-pid> <agent-jar-path>
*
* Java9+ note: tools.jar is gone; the AttachAPI is in jdk.attach module:
* java --add-modules jdk.attach LiveAttachDemo <pid> <agent.jar>
*/
public class LiveAttachDemo {\n\n public static void main(String[] args) throws Exception {\n if (args.length < 2) {\n System.err.println(\"Usage: LiveAttachDemo <target-pid> <path-to-agent.jar>\");\n System.exit(1);\n }\n\n String targetPid = args[0];\n String agentJarPath = args[1];\n\n // List all JVMs on this machine so the user can verify the PID\n System.out.println(\"\\n=== JVMs currently running on this machine ===\");\n List<VirtualMachineDescriptor> jvmList = VirtualMachine.list();\n for (VirtualMachineDescriptor descriptor : jvmList) {\n System.out.printf(\" PID: %-8s Display: %s%n\",\n descriptor.id(), descriptor.displayName());\n }\n System.out.println();\n\n // Attach to the target JVM — this opens a socket to its attach listener\n System.out.println(\"[Attacher] Attaching to PID: \" + targetPid);\n VirtualMachine targetJvm = VirtualMachine.attach(targetPid);\n\n try {\n // The JVM will call agentmain() in our TimingAgent class\n // The second argument (agentOptions) becomes the agentArgs param in agentmain\n String agentOptions = \"io/codeforge/demo\"; // package prefix to instrument\n targetJvm.loadAgent(agentJarPath, agentOptions);\n System.out.println(\"[Attacher] Agent loaded successfully into PID \" + targetPid);\n\n } finally {\n // Always detach to close the socket — a leaked attach can prevent\n // future attaches and consume file descriptors\n targetJvm.detach();\n System.out.println(\"[Attacher] Detached cleanly.\");\n }\n }\n}","output": "=== JVMs currently running on this machine ===\n PID: 18443 Display: io.codeforge.demo.OrderServiceApp\n PID: 22901 Display: LiveAttachDemo\n PID: 31042 Display: org.gradle.launcher.daemon.bootstrap.GradleDaemon\n\n[Attacher] Attaching to PID: 18443\n[Attacher] Agent loaded successfully into PID 18443\n[Attacher] Detached cleanly.\n\n--- Output from PID 18443 (the target JVM) ---\n[TimingAgent] Agent loaded. JVM classes currently loaded: 5,847\n[TimingAgent] ClassFileTransformer registered. Ready.\n[TimingAgent] Instrumented: io/codeforge/demo/OrderService\n[TimingReporter] io/codeforge/demo/OrderService#processOrder => 6,102,887 ns (6.1 ms)"
}
Shading Dependencies: Why Your Agent JAR Must Bundle Its Own ASM
One of the most common production failures with Java agents is a NoSuchMethodError or ClassNotFoundException for ASM classes at class load time. The root cause is a version conflict between the ASM version your agent bundles and the ASM version already on the application classpath — often from Spring, Hibernate, or another framework.
The solution is **shading**: repackaging your dependencies under a different package namespace so they don't collide. Use the Maven Shade plugin or Gradle Shadow plugin to relocate ASM from org.objectweb.asm to io.thecodeforge.shaded.org.objectweb.asm. Then update your transformer code to use the shaded imports.
Without shading, the JVM's classloader picks whichever ASM version loads first. If Spring's older ASM loads first, your agent's calls to newer ASM APIs fail with NoSuchMethodError. If your agent's ASM loads first, Spring breaks. Shading avoids this entirely.
After shading, your agent JAR contains ASM classes under io.thecodeforge.shaded.org.objectweb.asm. No conflicts with application's ASM.
⚠ Don't Forget to Update Your Imports
After shading, you must update your transformer code to import from the shaded package (e.g., import io.thecodeforge.shaded.org.objectweb.asm.*). For ByteBuddy users, the ByteBuddy Agent Builder automatically handles shading. If you forget, your agent will still compile but fail at runtime with ClassNotFoundException.
📊 Production Insight
ASM version conflicts cause silent startup failures that are notoriously hard to debug.
The error appears as VerifyError or NoSuchMethodError unrelated to your logic.
Shading is the only reliable fix — don't rely on classloader isolation hacks.
Always test your agent JAR with a real application that includes popular frameworks.
🎯 Key Takeaway
Shade ASM into your own package to avoid classpath conflicts.
Update imports after shading.
Test against Spring/Hibernate before shipping.
Static vs Dynamic Loading — When -javaagent Bites You in Production
The JVM gives you two ways to load an agent: premain (static) and agentmain (dynamic). Static loading uses the -javaagent flag at startup. The agent gets a premain method and runs before your main class. Dynamic loading uses the Attach API to hook into a running JVM. It calls agentmain instead.
Static loading is simpler. You drop the flag, the agent transforms classes as they load. No retransformation needed. But here's the trap: static agents see every class once. If you need to instrument a class that was loaded before your agent attached, you're screwed. That's where dynamic loading helps. You can retransform already-loaded classes using retransformClasses() on the Instrumentation instance.
Dynamic loading sounds great until you hit classloader hell. The Attach API requires the target JVM to have a compatible agent. Different JDK distributions handle this differently. OpenJDK-based JVMs work fine. IBM J9? Good luck. Also, dynamic loading can trigger ClassCircularityError if your transformer accidentally triggers class loading during a transform. That's a recursive nightmare that kills your JVM cold.
VirtualMachine.attach() can hang indefinitely if the target JVM is not responding. Always wrap it in a timeout thread. Default timeout? There isn't one. You've been warned.
🎯 Key Takeaway
Use static loading for startup instrumentation. Use dynamic loading only when you absolutely need to instrument a class that was loaded before your agent existed — and be ready for classloader recursion.
Writing a Transformer That Doesn't Blow Up — ClassLoader Context Matters
Your ClassFileTransformer runs inside the class loading chain. That means your transformer's own classes must not trigger a class load that your transformer is expected to transform. If they do, you get a stall or a crash.
Rule one: never use the transformed class's ClassLoader to load anything in your transformer. If you do, you'll deadlock when that ClassLoader tries to load the transformer's helper class while the transformer holds the lock. Use a separate thread or pre-load everything in premain/agentmain.
Rule two: your transformer must be stateless unless you've synchronized access. Multiple threads can call transform() concurrently. If you're collecting metrics inside the transformer, use a ConcurrentHashMap or an atomic counter. Don't allocate memory inside transform() unless you want to flood the young gen with garbage. Pre-allocate buffers or use ThreadLocal pools.
Rule three: return null from transform() if you don't need to modify a class. Returning the original byte array is fine but wastes cycles. Returning null tells the JVM to skip retransformation entirely. That's faster. Every microsecond in transform() delays application startup by seconds in aggregate.
(No output unless transformer triggers class loading — then you get a ClassCircularityError)
💡Senior Shortcut: Skip java/* Classes Entirely
🎯 Key Takeaway
Your transformer is part of the class loading chain. Keep it stateless, stateless, stateless. Return null for classes you don't need. Never allocate inside transform().
Agent Security — Why Your Agent Is a Backdoor Unless You Lock It Down
A Java agent runs with the same permissions as the JVM. That means your agent can read files, open sockets, and execute arbitrary code. If someone drops a malicious JAR into your classpath with an agent manifest, congratulations — you've just handed them the keys to your kingdom.
Security managers are deprecated starting Java 17. That old safety net is gone. The replacement is not here yet. So you need to validate what gets loaded. Sign your agent JAR and verify the signature before loading. Use a whitelist of allowed packages that your transformer will touch. Never transform classes outside that whitelist.
Another attack vector: the Attach API requires no authentication by default. If you're running on a shared host or in a container, any code with access to the JVM's temporary directory can attach an agent. Use Java's built-in agent attach permission check, or disable the Attach API entirely by setting the system property jdk.attach.allowAttachSelf=false.
For production, run a security audit on any third-party agent before deployment. OpenTelemetry, New Relic, AppDynamics — they all use the same API. One vulnerability in a popular agent can compromise thousands of deployments. Don't trust blindly.
Always verify the integrity of any agent JAR before loading it, especially in shared environments. The Attach API doesn't check signatures. If an attacker replaces your agent JAR, they own your JVM.
🎯 Key Takeaway
Agent code runs with JVM-level trust. Sign your JAR, verify on load, disable the Attach API in multi-tenant environments. Security is not optional — it's table stakes.
Comparison to Other Approaches — AOP Frameworks, Proxies, and Compile-Time Weaving
Java agents aren't the only way to intercept or augment behavior. Before committing, compare how alternatives solve the same problem. Spring AOP uses runtime proxies (JDK dynamic proxies or CGLIB) — it only intercepts Spring beans and fails on final classes or self-invocation. Compile-time weaving (AspectJ with ajc) modifies bytecode at build time, giving you full control over join points without runtime overhead, but requires a different compilation pipeline and can't alter code you don't recompile. Byte-buddy agent-style instrumentation is the only approach that modifies any class loaded by the JVM, including third-party libraries and JDK internals, without source access or build changes. The trade-off: agents operate at class-load time and must handle concurrency, retransformation limits, and class loader isolation. Performance is comparable once applied, but agent startup and retransformation are heavier. Choose agents when you need system-wide interception, cannot control the build, or need runtime attach. Prefer proxies for simple bean-level logging and AspectJ for compile-time guarantees.
AgentVsAspect.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// io.thecodeforge — java tutorial// Proxy approach: only intercepts Spring beanspublicinterfaceService {
voidexecute();
}
// Agent approach: intercepts every Service implementation// even ones you never compiledpublicclassMyAgent {
publicstaticvoidpremain(String args, Instrumentation inst) {
inst.addTransformer((loader, name, cl, pd, buf) -> {
// modify bytecode of every classreturntransform(buf);
});
}
}
// AspectJ compile-time: requires ajc compiler// aspect Logging {// pointcut call(): execution(* Service.*(..));// before(): call() { System.out.println("called"); }// }
Output
Agent: system-wide, any class, runtime attach.
Proxy: Spring beans only, no final classes.
AspectJ: compile-time, full control, no runtime attach.
⚠ Production Trap:
If you only need to intercept your own beans, agents add complexity without benefit. Start with proxies. Upgrade to agents only when you must instrument external libraries or JDK classes.
🎯 Key Takeaway
Choose the interception technique that matches your scope: proxies for beans, AspectJ for build-time, agents for any class anywhere.
Java agents break silently. Here are the three worst pitfalls and how to fix them. First, class loader leaks: your transformer holds a reference to a ClassLoader, preventing GC of that loader and its classes. Always store loader as a WeakReference or clear mappings in transformer‘s transform method after processing. Second, retransformation limits: the JVM caps how many times you can retransform a class (default 100). Hitting this in production causes a Throwable that crashes the agent. Track retransformation counts per class and avoid repeated retransformations. Third, agent conflicts: two agents using different ASM versions can corrupt each other’s bytecode. Shade your own ASM copy and rename packages to avoid classpath clashes. Bonus: transformer exceptions are swallowed silently by the JVM. Wrap your transform method in a try-catch that logs errors and returns null to let the original bytecode load. Test with a production-like class load order — agents fail most often when transforming classes that are only present in certain environments.
Never throw in transform(). Return null on error to avoid breaking class loading.
⚠ Production Trap:
Silent failures: the JVM prints nothing when your transformer throws. Always log errors inside transform() and return null. Missing logs = production outage with zero visibility.
🎯 Key Takeaway
Defensive programming in transformers is non-negotiable: guard against leaks, retransform limits, and swallowed exceptions.
Introduction
Java agents are the JVM’s rawest form of runtime introspection — a weapon that rewrites bytecode before the classloader even sees it. Unlike proxies or AOP frameworks, agents operate at the bytecode level, meaning they can intercept constructors, native methods, and even java.lang.System calls. The core mechanism is simple: a premain or agentmain method receives an Instrumentation instance, and you register a ClassFileTransformer that mutates class bytes. This powers everything from profilers (YourKit) to APM tools (Datadog) to debuggers (IntelliJ’s hot-reload). But raw power demands respect: a single malformed bytecode instruction crashes the JVM. Why should you care? Because when you need to measure latency on every HTTP request without touching source code, or inject security checks into a third-party library, agents are the only sane path. This section grounds you in the mental model — an agent is a plugin to the JVM’s class-loading pipeline, not magic.
SimpleAgent.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — java tutorial// Minimal agent: prints class names as they loadimport java.lang.instrument.*;
publicclassSimpleAgent {
publicstaticvoidpremain(String args, Instrumentation inst) {
inst.addTransformer(newClassFileTransformer() {
@Overridepublicbyte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if (className != null && className.startsWith("com/example")) {
System.out.println("Loading: " + className);
}
return null; // no modification
}
});
}
}
Output
Loading: com/example/MyService
Loading: com/example/MyController
⚠ Production Trap:
Returning null from transform() means 'no change' — the JVM uses original bytes. Returning an empty array corrupts the class and crashes.
🎯 Key Takeaway
An agent hooks into class loading; your transformer must return null for untouched classes.
Conclusion
Java agent instrumentation is the sharpest tool in the JVM toolbelt — it redefines what’s possible in production observability, security, and hot-patching without modifying application code. Throughout this guide, you’ve seen that agents are not a black box: they rely on precise manifest setup, careful bytecode manipulation via ASM or ByteBuddy, and a deep respect for classloader boundaries. The critical insight is that every bytecode transformation carries risk — a misplaced instruction in a hot method introduces memory leaks, deadlocks, or SilentClassCastExceptions that only surface under load. The standard approach? Shade all dependencies to avoid conflicts, always handle retransformation gracefully (your transformer must be idempotent), and lock down your agent with signed JARs and permission checks. When done right, agents provide capabilities that no framework can match: modifying java.lang classes, instrumenting lambda internals, and attaching to a running JVM without restart. The cost is complexity — but for the senior engineer, that’s the price of ultimate control.
null if already instrumented, else modified bytecode
⚠ Production Trap:
Retransforming an already-instrumented class without idempotency check doubles bytecode size and kills performance.
🎯 Key Takeaway
Always guard against double-instrumentation — use a marker annotation or a version stamp in bytecode.
● Production incidentPOST-MORTEMseverity: high
Production Outage: Agent's COMPUTE_FRAMES Crashes JVM on Class Load
Symptom
After deploying a new version of the monitoring agent, the application took 10x longer to start, then began throwing ClassNotFoundException for classes that clearly existed on the classpath. No traffic could be served.
Assumption
We assumed the agent was safe because it had been running for months without issues — the new version only added instrumentation for a new library.
Root cause
The transformer used ClassWriter.COMPUTE_FRAMES but didn't pass the original ClassLoader from the transform() method signature. ASM internally loads referenced classes to compute stack map frames, and without the correct ClassLoader, it couldn't find the new library's classes, causing the transform to throw an exception that was silently caught (returning null), which then cascaded as later transformers and class resolution failed.
Fix
Modified the transformer to pass the ClassLoader from the transform parameters into the ClassWriter constructor explicitly. Also added logging at every transform failure with the ClassLoader identity to aid future debugging.
Key lesson
Always pass the original ClassLoader to ASM's ClassWriter when using COMPUTE_FRAMES.
Never rely on the context ClassLoader — it may be null for bootstrap classes.
Log all transform exceptions with full stack traces — silenced failures mask production outages.
Test agent updates against a representative workload before rolling to production.
Production debug guideSymptom-Action pairs for the most common agent failures5 entries
Symptom · 01
Agent JAR loaded but transformer never called
→
Fix
Verify MANIFEST.MF has Premain-Class (or Agent-Class) pointing to the correct class. Use unzip -p agent.jar META-INF/MANIFEST.MF to inspect. Check JVM flags for typos in -javaagent path.
Symptom · 02
Class loads throw VerifyError after transformation
→
Fix
Check if you used COMPUTE_FRAMES. If manual stack frames are wrong, the verifier rejects the class. Also verify you're not corrupting the constant pool or changing method signatures.
Symptom · 03
ClassNotFoundException when using COMPUTE_FRAMES
→
Fix
Pass the ClassLoader from the transform() method directly to ClassWriter constructor. If using a different ClassLoader, the transform may fail to resolve types during frame computation.
Symptom · 04
Agent loads but instrumentation has no effect on certain classes
→
Fix
Confirm classes are being loaded after the transformer is added. For classes loaded before agent attach (via -javaagent), retransformation is required. Check if Can-Retransform-Classes is true in MANIFEST.MF.
Symptom · 05
Attach API fails with 'java.io.IOException: Operation not supported'
→
Fix
Self-attach requires -Djdk.attach.allowAttachSelf=true on Java 9+. For remote attach, ensure the target JVM's attach listener is enabled (default on, but some security managers disable it).
★ Quick Agent Debugging Cheat SheetThree-command patterns for the most critical agent failures
java -cp .:$JAVA_HOME/lib/tools.jar com.sun.tools.attach.VirtualMachine list
jcmd <PID> JVMTI.data_dump
Fix now
Run the attach process with --add-modules jdk.attach on Java 9+ and ensure tools.jar is on classpath for Java 8.
ASM vs ByteBuddy vs Javassist
Feature
ASM
ByteBuddy
Javassist
API Level
Instruction-level
High-level (fluent)
Source-level (String)
Learning Curve
Steep
Moderate
Shallow
Performance
Fastest
Very fast (on ASM)
Moderate
Stack Frame Handling
Manual or COMPUTE_FRAMES
Automatic
Automatic
Class Creation
Possible but manual
Built-in (DynamicType)
Easy (new ClassPool)
File Size (jar)
~500 KB
~1.5 MB
~800 KB
Production Use
JaCoCo, OpenTelemetry
Mockito, Hibernate
Spring AOP (CGLib replacement)
⚙ Quick Reference
13 commands from this guide
File
Command / Code
Purpose
TimingAgent.java
/**
How the JVM Loads a Java Agent
META-INFMANIFEST.MF
Manifest-Version: 1.0
Manifest File Setup Guide
ByteBuddyAgent.java
public class ByteBuddyAgent {
Bytecode Manipulation Libraries
MethodTimingTransformer.java
/**
Writing a ClassFileTransformer with ASM
LiveAttachDemo.java
/**
Runtime Attach, Class Retransformation, and Production Perfo
pom.xml (shade plugin config)
Shading Dependencies
AgentLoadDecision.java
public class AgentLoadDecision {
Static vs Dynamic Loading
SafeTransformer.java
public class SafeTransformer implements ClassFileTransformer {
Writing a Transformer That Doesn't Blow Up
AgentSecurityCheck.java
public class AgentSecurityCheck {
Agent Security
AgentVsAspect.java
public interface Service {
Comparison to Other Approaches
PitfallFix.java
public class SafeTransformer implements ClassFileTransformer {
java configuration
SimpleAgent.java
public class SimpleAgent {
Introduction
SafeTransformer.java
public byte[] transform(ClassLoader loader, String className,
Conclusion
Key takeaways
1
Java agents intercept class loading via ClassFileTransformer.transform(), which runs in the system class loader context
not the target class's loader, causing class resolution failures during ASM's COMPUTE_FRAMES.
2
ASM's COMPUTE_FRAMES option triggers class loading for supertypes and interfaces; if the class loader is unavailable or garbage collected, the JVM crashes with ClassNotFoundException or NullPointerException.
3
Production agents like ByteBuddy default to COMPUTE_MAXS instead of COMPUTE_FRAMES to avoid frame computation crashes, accepting manual frame metadata for correctness.
4
The agent JAR's MANIFEST.MF must declare Premain-Class (and optionally `Can-Retransform-Classes
true`); missing or incorrect attributes cause the JVM to silently ignore the agent.
5
Dynamic agent attachment via the Attach API works on running JVMs, but self-attach on Java 9+ requires -Djdk.attach.allowAttachSelf=true or it throws an IOException.
Common mistakes to avoid
4 patterns
×
Using depends_on without a healthcheck in Docker Compose
Symptom
Container crashes on startup with ECONNREFUSED because the database container started but is not yet ready.
Fix
Add a healthcheck block to the database service using pg_isready, then use condition: service_healthy in the client service.
×
Not shading ASM in the agent JAR
Symptom
NoSuchMethodError or VerifyError when the application also uses ASM (e.g., Spring, Hibernate).
Fix
Use Maven Shade or Gradle Shadow to relocate org.objectweb.asm to your own package namespace.
×
Forgetting Can-Retransform-Classes: true in MANIFEST.MF
Symptom
Calling retransformClasses() throws UnsupportedOperationException even though transformer is registered.
Fix
Add 'Can-Retransform-Classes: true' to MANIFEST.MF and rebuild the JAR.
×
Not passing the ClassLoader to ClassWriter with COMPUTE_FRAMES
Symptom
ClassNotFoundException during class transformation when ASM tries to load referenced types.
Fix
Pass the ClassLoader parameter from the transform() method signature directly to the ClassWriter constructor.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
Explain how a Java agent works from JVM startup to bytecode transformati...
Q02SENIOR
What is the difference between COMPUTE_MAXS and COMPUTE_FRAMES in ASM's ...
Q03SENIOR
Describe a production incident caused by a Java agent and how you fixed ...
Q01 of 03JUNIOR
Explain how a Java agent works from JVM startup to bytecode transformation.
ANSWER
At startup, the JVM reads the -javaagent flag and loads the agent JAR. It looks for the Premain-Class attribute in MANIFEST.MF, then calls the premain(String, Instrumentation) method. The Instrumentation instance allows registering a ClassFileTransformer. The transformer is called each time a class is loaded. It receives the class byte array and can return a modified version using a library like ASM. The JVM then uses the transformed bytecode to define the class.
Q02 of 03SENIOR
What is the difference between COMPUTE_MAXS and COMPUTE_FRAMES in ASM's ClassWriter? When would you use each?
ANSWER
COMPUTE_MAXS only computes maximum stack depth and local variable counts. It's faster but assumes you correctly maintain stack map frames manually. COMPUTE_FRAMES computes everything including stack map frames, which is safer but slower. Use COMPUTE_MAXS for simple transformations that don't change control flow. Use COMPUTE_FRAMES for complex transformations that add branches, loops, or method calls. COMPUTE_FRAMES can also cause class loading side effects because it needs to load referenced types to compute frames.
Q03 of 03SENIOR
Describe a production incident caused by a Java agent and how you fixed it.
ANSWER
We deployed a monitoring agent that used ClassWriter.COMPUTE_FRAMES but didn't pass the original ClassLoader from the transform() method. ASM's frame computation tried to load referenced classes using the wrong ClassLoader, causing ClassNotFoundException. The transformer caught the exception and returned null, silently falling back to the original class. This caused the agent to not instrument any classes, and the missing instrumentation led to cascading failures in our monitoring pipeline. We fixed it by passing the ClassLoader parameter into the ClassWriter constructor and adding logging for all transform failures.
01
Explain how a Java agent works from JVM startup to bytecode transformation.
JUNIOR
02
What is the difference between COMPUTE_MAXS and COMPUTE_FRAMES in ASM's ClassWriter? When would you use each?
SENIOR
03
Describe a production incident caused by a Java agent and how you fixed it.
SENIOR
FAQ · 3 QUESTIONS
Frequently Asked Questions
01
Can I use a Java agent to add new methods or fields to existing classes?
No, the Instrumentation API's retransform feature can only change method bodies. Adding new methods or fields is not supported because it would require changing the class schema, which the JVM's hot-swap mechanism cannot do. Use separate proxy classes or dependency injection to add new behavior.
Was this helpful?
02
Why does my agent cause a VerifyError even though I used COMPUTE_FRAMES?
VerifyError can still occur if you corrupt the constant pool or change method signatures. COMPUTE_FRAMES only fixes stack map frames. Also, ensure you passed the correct ClassLoader to the ClassWriter — without it, frame computation can fail silently and produce corrupt frames. Use -XX:-UseSplitVerifier temporarily (Java 8 only) to diagnose.
Was this helpful?
03
How do I attach a Java agent to a running process in a Docker container?
The Attach API uses a socket file in /tmp/.java_pid<pid>. If the container doesn't share /tmp with the host, the attach fails. Use -XX:+StartAttachListener to start the listener early, and mount a shared volume or use host networking. Alternatively, start the agent at container startup with -javaagent.