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.

Architect's Guide
Architect's Guide
Architect's Guide
How to Use Java Agents and Instrumentation for Non‑Intrusive Performance Monitoring

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.PrintNumTest

Attaching the agent later:

java -cp $JAVA_HOME/lib/tools.jar:/path/to/aop-demo.jar com.example.aop.agent.MyAttachMain 49987

Arthas 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.

Arthas trace command flow
Arthas trace command flow

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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaInstrumentationPerformance MonitoringJava AgentArthasAttach API
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.