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 StopWatch to log execution time.
@Override
public void method(Req req) {
StopWatch stopWatch = new StopWatch();
stopWatch.start("某某方法-耗时统计");
method();
stopWatch.stop();
log.info("查询耗时分布:{}", stopWatch.prettyPrint());
}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.instrument package provides tools for bytecode enhancement via the Instrumentation interface. Common methods include:
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();
}There are two ways to use it:
Add an Agent jar when the JVM starts.
After the JVM is running, load an agent jar remotely via the Attach API.
Agent
The premain method runs before main() during JVM startup. Inside it we can register a ClassFileTransformer to modify class bytecode.
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("premain方法");
instrumentation.addTransformer(new MyClassFileTransformer(), true);
}Key transformer classes:
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);
}
}Build the agent with Maven (assembly plugin to create a jar‑with‑dependencies) and run the test class:
java -javaagent:/path/to/aop-demo.jar com.example.aop.agent.MyTestThe output shows the method execution time.
Attach
When the JVM is already running, agentmain is invoked after the agent is attached. Its signature mirrors premain:
public static void agentmain(String agentArgs, Instrumentation inst) { }Using agentmain, we can retransform classes at any time. The example modifies a class that continuously prints 100 to print 50 instead.
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; }
}Transformer that replaces the return value:
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);
}
}Agent that registers the transformer and triggers retransform:
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();
}
}
}Run the target program, find its PID with jps, then attach:
java -cp $JAVA_HOME/lib/tools.jar:/path/to/aop-demo.jar com.example.aop.agent.MyAttachMain 49987Arthas
Arthas’s trace command 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:
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);
}
}The target method to be enhanced:
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;
}
}After bytekit processing, the decompiled result contains inserted calls to SpyAPI.atEnter, atExit, and exception handling code, dramatically increasing the method size.
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;
}
}trace
The trace command’s implementation follows the template pattern: TraceCommand extends EnhancerCommand, which registers a transformer that inserts listeners via AdviceListenerManager.registerTraceAdviceListener. The core transformer logic resides in Enhancer#enhance, which parses interceptor classes, creates InterceptorProcessor objects, and modifies the target method bytecode.
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;
}Two main steps are performed:
Parse interceptor classes to collect @AtXxx annotations and build InterceptorProcessor objects.
Iterate over matched methods and let each InterceptorProcessor modify 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
