How to Use Java Agents for Non‑Intrusive Method Timing and Bytecode Tracing
This article explains how to replace invasive timing code with a Java Agent using the Instrumentation API, demonstrates both premain and attach approaches, shows ASM‑based bytecode transformation examples, and explores how Arthas trace leverages similar techniques for runtime monitoring.
Story's Little Yellow Flower
A teammate was adding massive timing code to methods because the infrastructure lacked performance metrics, using
StopWatchto log execution time.
<code>@Override
public void method(Req req) {
StopWatch stopWatch = new StopWatch();
stopWatch.start("某某方法-耗时统计");
method();
stopWatch.stop();
log.info("查询耗时分布:{}", stopWatch.prettyPrint());
}</code>Seeing the high intrusiveness, we turned to Java Agent technology for a non‑intrusive solution and built a tiny demo.
Instrumentation
Since JDK 1.5, the
java.lang.instrumentpackage provides tools for bytecode enhancement via the
Instrumentationinterface. Common methods include:
<code>public interface Instrumentation {
/** Register a class file transformer */
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
/** Remove a transformer */
boolean removeTransformer(ClassFileTransformer transformer);
/** Retransform already loaded classes */
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
/** Check if retransformation is supported */
boolean isRetransformClassesSupported();
/** Get all loaded classes */
Class[] getAllLoadedClasses();
}</code>There are two ways to use it:
Add an
Agent jarwhen the JVM starts.
After the JVM is running, load an agent jar remotely via the
Attach API.
Agent
The
premainmethod runs before
main()during JVM startup. Inside it we can register a
ClassFileTransformerto modify class bytecode.
<code>public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("premain方法");
instrumentation.addTransformer(new MyClassFileTransformer(), true);
}</code>Key transformer classes:
<code>public class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if ("com/example/aop/agent/MyTest".equals(className)) {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new TimeStatisticsVisitor(Opcodes.ASM7, cw);
cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
return cw.toByteArray();
}
return classfileBuffer;
}
}
public class TimeStatisticsVisitor extends ClassVisitor {
// ... uses ASM to insert start/end calls
}
public class TimeStatistics {
static ThreadLocal<Long> t = new ThreadLocal<>();
public static void start() { t.set(System.currentTimeMillis()); }
public static void end() {
long time = System.currentTimeMillis() - t.get();
System.out.println(Thread.currentThread().getStackTrace()[2] + " spend: " + time);
}
}
public class AgentMain {
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("premain方法");
instrumentation.addTransformer(new MyClassFileTransformer(), true);
}
}</code>Build the agent with Maven (assembly plugin to create a jar‑with‑dependencies) and run the test class:
<code>java -javaagent:/path/to/aop-demo.jar com.example.aop.agent.MyTest</code>The output shows the method execution time.
Attach
When the JVM is already running,
agentmainis invoked after the agent is attached. Its signature mirrors
premain:
<code>public static void agentmain(String agentArgs, Instrumentation inst) { }</code>Using
agentmain, we can retransform classes at any time. The example modifies a class that continuously prints
100to print
50instead.
<code>public class PrintNumTest {
public static void main(String[] args) throws InterruptedException {
while (true) {
System.out.println(getNum());
Thread.sleep(3000);
}
}
private static int getNum() { return 100; }
}</code>Transformer that replaces the return value:
<code>public class PrintNumTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if ("com/example/aop/agent/PrintNumTest".equals(className)) {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new TransformPrintNumVisitor(Opcodes.ASM7, cw);
cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
return cw.toByteArray();
}
return classfileBuffer;
}
}
public class TransformPrintNumVisitor extends ClassVisitor {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if ("getNum".equals(name)) {
return new TransformPrintNumAdapter(api, mv, access, name, descriptor);
}
return mv;
}
}
public class TransformPrintNumAdapter extends AdviceAdapter {
@Override
protected void onMethodEnter() {
super.visitIntInsn(BIPUSH, 50);
super.visitInsn(IRETURN);
}
}</code>Agent that registers the transformer and triggers retransform:
<code>public class PrintNumAgent {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
System.out.println("agentmain");
inst.addTransformer(new PrintNumTransformer(), true);
for (Class c : inst.getAllLoadedClasses()) {
if (c.getSimpleName().equals("PrintNumTest")) {
System.out.println("Reloading: " + c.getName());
inst.retransformClasses(c);
break;
}
}
}
}
public class MyAttachMain {
public static void main(String[] args) throws Exception {
VirtualMachine vm = VirtualMachine.attach(args[0]);
try {
vm.loadAgent("/path/to/aop-demo.jar");
} finally {
vm.detach();
}
}
}</code>Run the target program, find its PID with
jps, then attach:
<code>java -cp $JAVA_HOME/lib/tools.jar:/path/to/aop-demo.jar com.example.aop.agent.MyAttachMain 49987</code>Arthas
Arthas’s
tracecommand uses the same bytecode‑enhancement principle to record method execution time. The command is implemented as a class extending
EnhancerCommand, which registers a transformer that inserts listeners at method entry/exit.
Setup Debug Environment
Arthas can be debugged remotely via IDEA’s remote‑debug feature. Set the target JVM to listen on a socket (e.g.,
-Xrunjdwp:transport=dt_socket,server=y,address=8000) and attach the IDE.
bytekit
bytekit builds on ASM to provide a concise API for bytecode enhancement. A simple interceptor example:
<code>public class SampleInterceptor {
@AtEnter(inline = false, suppress = RuntimeException.class, suppressHandler = PrintExceptionSuppressHandler.class)
public static void atEnter(@Binding.This Object object, @Binding.Class Object clazz,
@Binding.Args Object[] args, @Binding.MethodName String methodName,
@Binding.MethodDesc String methodDesc) {
System.out.println("atEnter, args[0]: " + args[0]);
}
@AtExit(inline = true)
public static void atExit(@Binding.Return Object returnObject) {
System.out.println("atExit, returnObject: " + returnObject);
}
@AtExceptionExit(inline = true, onException = RuntimeException.class,
suppress = RuntimeException.class, suppressHandler = PrintExceptionSuppressHandler.class)
public static void atExceptionExit(@Binding.Throwable RuntimeException ex,
@Binding.Field(name = "exceptionCount") int exceptionCount) {
System.out.println("atExceptionExit, ex: " + ex.getMessage() + ", field exceptionCount: " + exceptionCount);
}
}</code>The target method to be enhanced:
<code>public class Sample {
private int exceptionCount = 0;
public String hello(String str, boolean exception) {
if (exception) {
exceptionCount++;
throw new RuntimeException("test exception, str: " + str);
}
return "hello " + str;
}
}</code>After bytekit processing, the decompiled result contains inserted calls to
SpyAPI.atEnter,
atExit, and exception handling code, dramatically increasing the method size.
<code>public String hello(String string, boolean bl) {
try {
SpyInterceptor.atEnter(this, Sample.class, new Object[]{string, new Boolean(bl)}, "hello", "(Ljava/lang/String;Z)Ljava/lang/String;");
if (bl) {
++this.exceptionCount;
throw new RuntimeException("test exception, str: " + string);
}
String result = "hello " + string;
System.out.println("atExit, returnObject: " + result);
return result;
} catch (RuntimeException e) {
System.out.println("atExceptionExit, ex: " + e.getMessage() + ", field exceptionCount: " + this.exceptionCount);
throw e;
}
}</code>trace
The
tracecommand’s implementation follows the template pattern:
TraceCommandextends
EnhancerCommand, which registers a transformer that inserts listeners via
AdviceListenerManager.registerTraceAdviceListener. The core transformer logic resides in
Enhancer#enhance, which parses interceptor classes, creates
InterceptorProcessorobjects, and modifies the target method bytecode.
<code>public synchronized EnhancerAffect enhance(Instrumentation inst) throws UnmodifiableClassException {
// add this enhancer as a transformer
ArthasBootstrap.getInstance().getTransformerManager().addTransformer(this, isTracing);
// ... load class, parse interceptors, apply them, dump class if needed ...
return affect;
}</code>Two main steps are performed:
Parse interceptor classes to collect
@AtXxxannotations and build
InterceptorProcessorobjects.
Iterate over matched methods and let each
InterceptorProcessormodify the method’s bytecode.
The overall workflow is illustrated in the diagram above, and a concrete interceptor example (
SpyInterceptor1) shows how a simple static method is injected at method entry.
In summary, Java Agents, the Instrumentation API, ASM, and helper libraries like bytekit enable powerful, non‑intrusive runtime instrumentation, which tools such as Arthas leverage to provide real‑time tracing and performance monitoring.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.