Mastering Java Instrumentation: Non‑Intrusive Method Timing with Agents and Arthas

This article explains how to replace invasive manual timing code with Java Instrumentation, demonstrating both premain and agentmain approaches, building and attaching agents, using ASM and Bytekit for bytecode enhancement, and leveraging Arthas for runtime tracing and debugging.

Architect
Architect
Architect
Mastering Java Instrumentation: Non‑Intrusive Method Timing with Agents and Arthas

Problem Statement

Team members often insert repetitive timing code to measure method execution time, which is intrusive and hard to maintain.

Java Instrumentation Overview

Since JDK 1.5, the java.lang.instrument package provides tools for bytecode manipulation. The Instrumentation interface offers methods such as addTransformer, removeTransformer, retransformClasses, and getAllLoadedClasses.

public interface Instrumentation {
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
    boolean removeTransformer(ClassFileTransformer transformer);
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    boolean isRetransformClassesSupported();
    Class[] getAllLoadedClasses();
}

Using Java Agent (premain)

The premain method runs before main(). By registering a ClassFileTransformer inside premain, you can modify class bytecode during class loading.

public static void premain(String agentArgs, Instrumentation instrumentation) {
    System.out.println("premain method");
    instrumentation.addTransformer(new MyClassFileTransformer(), true);
}

Attaching Agent at Runtime (agentmain)

The agentmain method is invoked when an agent is attached to a running JVM via the Attach API. It allows dynamic re‑transformation of already loaded classes.

public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
    System.out.println("agentmain");
    inst.addTransformer(new PrintNumTransformer(), true);
    for (Class loaded : inst.getAllLoadedClasses()) {
        if (loaded.getSimpleName().equals("PrintNumTest")) {
            System.out.println("Reloading: " + loaded.getName());
            inst.retransformClasses(loaded);
            break;
        }
    }
}

Building the Agent JAR

The Maven assembly plugin can create a fat JAR that includes all dependencies. The manifest must specify Premain-Class and Agent-Class entries.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.1.1</version>
    <configuration>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <archive>
            <manifestEntries>
                <Agent-Class>com.example.aop.agent.AgentMain</Agent-Class>
                <Premain-Class>com.example.aop.agent.AgentMain</Premain-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

Testing the Agent

Run the target application with -javaagent to trigger premain, or use a separate program to attach the agent at runtime.

java -javaagent:/path/to/aop-0.0.1-SNAPSHOT-jar-with-dependencies.jar com.example.aop.agent.MyTest

public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(3000);
    }
}

After attaching, the printed number changes from 100 to 50 as shown below.

Effect
Effect

Arthas Debugging and Tracing

Arthas can attach to a JVM for remote debugging. By setting breakpoints in the source code (e.g., com.taobao.arthas.agent334.AgentBootstrap#main) and launching arthas-boot.jar, you can step into the Arthas source.

Remote Debug
Remote Debug

Bytekit for Simplified Bytecode Enhancement

Bytekit provides a concise API over ASM. An interceptor class can declare @AtEnter, @AtExit, and @AtExceptionExit methods to inject code at method entry, exit, or exception.

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) {
        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);
    }
}

Bytekit generates additional bytecode, turning the original method into a larger version that calls the interceptor methods.

public String hello(String str, boolean exception) {
    try {
        SampleInterceptor.atEnter(this, Sample.class, new Object[]{str, Boolean.valueOf(exception)}, "hello");
        if (exception) {
            this.exceptionCount++;
            throw new RuntimeException("test exception, str: " + str);
        }
        String result = "hello " + str;
        System.out.println("atExit, returnObject: " + result);
        return result;
    } catch (RuntimeException e) {
        System.out.println("atExceptionExit, ex: " + e.getMessage() + ", field exceptionCount: " + this.exceptionCount);
        throw e;
    }
}

Arthas Trace Command Implementation

The trace command is handled by TraceCommand, which extends EnhancerCommand. The core logic resides in Enhancer.enhance, which registers a ClassFileTransformer, parses interceptor classes, and modifies matching methods.

public byte[] transform(final ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain pd, byte[] buf) throws IllegalClassFormatException {
    // Load class node
    ClassNode cn = new ClassNode(Opcodes.ASM9);
    ClassReader cr = new ClassReader(buf);
    cr.accept(cn, 0);
    // Parse interceptors
    List<InterceptorProcessor> processors = ...;
    for (MethodNode mn : cn.methods) {
        if (!isIgnore(mn)) {
            MethodProcessor mp = new MethodProcessor(cn, mn);
            for (InterceptorProcessor ip : processors) {
                ip.process(mp);
            }
            AdviceListenerManager.registerAdviceListener(loader, className, mn.name, mn.desc, listener);
        }
    }
    return AsmUtils.toBytes(cn, loader, cr);
}

The transformer inserts listeners for method entry/exit and, when tracing, registers AdviceListener objects that collect timing information.

Trace Flow
Trace Flow

Overall Process Flow

The diagram below summarizes the steps: load interceptor classes, parse annotations, generate InterceptorProcessor objects, traverse class methods, apply bytecode modifications, and finally register advice listeners.

Overall Flow
Overall Flow

References

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.

javaInstrumentationPerformanceMonitoringArthastraceJavaAgent
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.