Backend Development 22 min read

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

.

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

Using Java Agent (premain)

The

premain

method runs before

main()

. By registering a

ClassFileTransformer

inside

premain

, you can modify class bytecode during class loading.

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

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.

<code>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;
        }
    }
}
</code>

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.

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

Testing the Agent

Run the target application with

-javaagent

to trigger

premain

, or use a separate program to attach the agent at runtime.

<code>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);
    }
}
</code>

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.

<code>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);
    }
}
</code>

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

<code>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;
    }
}
</code>

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.

<code>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);
}
</code>

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

JavaInstrumentationPerformanceMonitoringBytecodeArthasTraceJavaAgent
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

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