How to Use Java Agents and Instrumentation for Non‑Intrusive Performance Monitoring
This article explains why inserting manual timing code is invasive, introduces the java.lang.instrument API, shows how to write a tiny Java Agent with premain and agentmain methods, demonstrates dynamic class redefinition via the Attach API, and explores how tools like Arthas and Bytekit leverage these mechanisms for runtime tracing and bytecode enhancement.
Why Instrumentation?
In a typical performance‑optimization task, developers may sprinkle
@Override public void method(Req req) { StopWatch sw = new StopWatch(); sw.start("method"); method(); sw.stop(); log.info("cost: {}", sw.prettyPrint()); }throughout the code base, which is highly intrusive and hard to maintain.
Java Instrumentation API
Since JDK 1.5, the java.lang.instrument package provides tools for bytecode manipulation at runtime. The key interface Instrumentation offers methods such as
addTransformer(ClassFileTransformer transformer, boolean canRetransform), removeTransformer(ClassFileTransformer transformer), retransformClasses(Class<?>... classes), and getAllLoadedClasses(). A ClassFileTransformer receives the original class byte array, can modify it, and returns the transformed bytes.
public interface Instrumentation { void addTransformer(ClassFileTransformer transformer, boolean canRetransform); boolean removeTransformer(ClassFileTransformer transformer); void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; Class[] getAllLoadedClasses(); }Using premain (static agent)
The premain method runs before the application’s main method. By registering a ClassFileTransformer inside premain, you can instrument classes as they are loaded.
public static void premain(String agentArgs, Instrumentation inst) { System.out.println("premain"); inst.addTransformer(new MyClassFileTransformer(), true); }A minimal transformer that measures method execution time might look like:
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; } }Dynamic instrumentation with agentmain (Attach API)
The agentmain method is invoked when an agent is attached to a running JVM. This enables you to retransform already loaded classes.
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; } } }Running the target program:
java -javaagent:/path/to/aop-demo.jar com.example.aop.agent.PrintNumTestAttaching the agent later:
java -cp $JAVA_HOME/lib/tools.jar:/path/to/aop-demo.jar com.example.aop.agent.MyAttachMain 49987Arthas trace command
Arthas uses the same instrumentation mechanism. Its trace command registers a transformer that inserts listeners at method entry/exit. The core logic resides in EnhancerCommand, which builds a list of InterceptorProcessor objects (e.g., SpyTraceInterceptor1) and applies them to matched methods.
Bytekit – a lightweight ASM wrapper
Bytekit simplifies ASM bytecode enhancement with annotations such as @AtEnter, @AtExit, and @AtExceptionExit. Example interceptor:
public class SampleInterceptor { @AtEnter(inline = false, suppress = RuntimeException.class, suppressHandler = PrintExceptionSuppressHandler.class) public static void atEnter(@Binding.This Object obj, @Binding.Class Class<?> 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 ret) { System.out.println("atExit, returnObject: " + ret); } }When applied to a target class, Bytekit generates additional bytecode that calls the interceptor methods before and after the original logic.
Reference Materials
https://developer.aliyun.com/article/768074
https://arthas.aliyun.com/doc/trace.html#注意事项
https://blog.csdn.net/tianjindong0804/article/details/128423819
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.
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.
