How to Use Java Agents for Non‑Intrusive Method Timing and Tracing
This article demonstrates how to use Java Agent and the java.lang.instrument API to non‑intrusively measure method execution time, dynamically modify bytecode with ASM, leverage Attach for runtime instrumentation, and explore related tools like Arthas and Bytekit for tracing and debugging Java applications.
Story's Little Yellow Flower
Team members wrote many lines of code to record method execution time because of insufficient infrastructure, which is invasive. The article introduces a small demo using Java Agent to solve this problem without code changes.
@Override
public void method(Req req) {
StopWatch stopWatch = new StopWatch();
stopWatch.start("某某方法-耗时统计");
method();
stopWatch.stop();
log.info("查询耗时分布:{}", stopWatch.prettyPrint());
}Instrumentation
Before using an Agent you need to understand the Instrumentation interface introduced in JDK 1.5, which provides methods for bytecode enhancement.
public interface Instrumentation {
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
boolean removeTransformer(ClassFileTransformer transformer);
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
boolean isRetransformClassesSupported();
Class[] getAllLoadedClasses();
}
public interface ClassFileTransformer {
byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException;
}Instrumentation can be used in two ways:
Attach an Agent JAR when the JVM starts.
Load an Agent JAR at any time after the JVM is running via the Attach API.
Agent
A Java Agent defines a premain method that runs before the application's main method.
public static void premain(String agentArgs, Instrumentation instrumentation) {
}The two parameters are the startup arguments and the Instrumentation instance, which allows registration of class file transformers.
agentArgs – arguments passed when the JVM starts.
instrumentation – the Instrumentation instance used to register transformers.
The following diagram shows the JVM startup flow when an Agent is used:
Implement a ClassFileTransformer that modifies a method to record its execution time:
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 {
// ... implementation omitted for brevity ...
}
public class TimeStatisticsAdapter extends AdviceAdapter {
@Override
protected void onMethodEnter() {
super.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/aop/agent/TimeStatistics",
"start", "()V", false);
super.onMethodEnter();
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
super.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/aop/agent/TimeStatistics",
"end", "()V", false);
}
}
public class TimeStatistics {
private 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);
}
}Run the test class with the agent:
java -javaagent:/path/to/aop-demo.jar com.example.aop.agent.MyTestThe output shows the measured execution time:
Attach
When the application is already running, you can load an agent dynamically using the agentmain method.
public static void agentmain(String agentArgs, Instrumentation inst) {
}The following example changes a constantly printed number from 100 to 50 at runtime.
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; }
}A transformer modifies getNum to return 50:
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;
}
}Attach the agent to the running process:
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();
}
}
} java -cp $JAVA_HOME/lib/tools.jar:/path/to/aop-demo.jar com.example.aop.agent.MyAttachMain 49987The printed number changes to 50:
Arthas
The demo can also be replaced by Arthas' trace command, which records method execution time.
Setup Debug Environment
Arthas debugging relies on IDEA remote debugging. Example demo:
public class ArthasTest {
public static void main(String[] args) throws InterruptedException {
int i = 0;
while (true) {
Thread.sleep(2000);
print(i++);
}
}
public static void print(Integer content) {
System.out.println("Main print: " + content);
}
}Run with remote debug enabled:
java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000 com.example.aop.agent.ArthasTestbytekit
bytekit provides a concise API on top of ASM for bytecode enhancement. Example interceptor:
public class SampleInterceptor {
@AtEnter(inline = false, suppress = RuntimeException.class, suppressHandler = PrintExceptionSuppressHandler.class)
public static void atEnter(@Binding.ThisObject Object object,
@Binding.ClassObject Class<?> clazz,
@Binding.ArgsObject[] args,
@Binding.MethodNameString String methodName,
@Binding.MethodDescString 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)
public static void atExceptionExit(@Binding.Throwable RuntimeException ex,
@Binding.Field(name = "exceptionCount") int exceptionCount) {
System.out.println("atExceptionExit, ex: " + ex.getMessage() + ", field exceptionCount: " + exceptionCount);
}
}@AtEnter inserts code at method entry; @AtExit at method exit; @AtExceptionExit on exception exit.
inline=true inserts the code directly; inline=false calls the interceptor method.
suppress and suppressHandler wrap the inserted code with try/catch.
@AtExceptionExit adds try/catch handling for specified exceptions.
Target method to enhance:
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 enhancement, the decompiled code shows inserted logging statements and exception handling.
public class Sample {
private int exceptionCount = 0;
public String hello(String string, boolean bl) {
try {
SampleInterceptor.atEnter(this, Sample.class, new Object[]{string, Boolean.valueOf(bl)}, "hello", "(Ljava/lang/String;Z)Ljava/lang/String;");
} catch (RuntimeException runtimeException) {
System.out.println("exception handler: " + Sample.class);
runtimeException.printStackTrace();
}
if (bl) {
++this.exceptionCount;
throw new RuntimeException("test exception, str: " + string);
}
String string3 = "hello " + string;
System.out.println("atExit, returnObject: " + string3);
return string3;
}
}trace
Arthas implements the trace command via the EnhancerCommand class, which registers a transformer that inserts tracing advice into target methods.
public abstract class AnnotatedCommand {
public abstract void process(CommandProcess process);
}
public class TraceCommand extends EnhancerCommand { }
public abstract class EnhancerCommand extends AnnotatedCommand {
@Override
public void process(final CommandProcess process) {
process.interruptHandler(new CommandInterruptHandler(process));
process.stdinHandler(new QExitHandler(process));
enhance(process);
}
}The core logic parses interceptor classes, generates InterceptorProcessor objects, and modifies the original method bytecode accordingly.
Parse @AtXxx annotations and generate interceptor processors.
Iterate over processors to modify the original method bytecode.
Overall flow diagram:
Example interceptor used by Arthas:
public static class SpyInterceptor1 {
@AtEnter(inline = true)
public static void atEnter(@Binding.This Object target, @Binding.Class Class<?> clazz,
@Binding.MethodInfo String methodInfo, @Binding.Args Object[] args) {
SpyAPI.atEnter(clazz, methodInfo, target, args);
}
}The article concludes that while the trace command logic is now clear, deeper understanding of bytekit and low‑level bytecode manipulation still requires further study.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.
