How to Dynamically Modify Java Bytecode with ASM and JVM Instrumentation
This article explores how Java developers can use the ASM framework, the Instrument API, and JVM TI to dynamically generate and transform bytecode at runtime, enabling powerful debugging features like Evaluate and custom agents for class redefinition.
Curiosity about Debug
When first learning Java, the author was fascinated by IDEA's Debug feature, especially the Evaluate window that allows executing code at breakpoints to compute values or modify variables.
Initially, the author used Debug as a development tool, writing code in the Evaluate box, copying it back to the IDE once it worked, effectively treating Java like an interpreted language.
Since Java is a static language that requires compilation, the author wondered how code could be injected into a running JVM.
Later, after learning about reflection and bytecode, a colleague introduced Btrace, which uses the ASM framework and JVM TI, revealing similarities with the Evaluate feature.
ASM
To implement Evaluate, the first challenge is changing existing code behavior, which is achieved through dynamic bytecode manipulation.
Dynamic Bytecode Generation
Java source is compiled into .class bytecode files, which can be altered by parsing and modifying the binary data, allowing changes to class behavior.
Several libraries support dynamic bytecode generation, such as BCEL, Javassist, ASM, and CGLib, each with trade‑offs in complexity and performance.
ASM Framework
ASM is the most powerful among them, enabling dynamic class and method modifications, even redefining classes; CGLib itself is built on ASM.
Using ASM requires knowledge of Java bytecode and JVM instructions. The author mentions the "ASM Bytecode Outline" plugin for IDEA to view generated bytecode.
Switching to the "ASMified" view in the plugin shows the ASM code directly.
Common Methods
ASM uses the Visitor pattern: a ClassReader parses bytecode and passes it to a ClassVisitor, where methods like visitMethod() or visitAnnotation() can be overridden to manipulate class structure.
A ClassWriter (which extends ClassVisitor) writes the modified bytecode.
Instrument
Introduction
After modifying bytecode, the JVM still loads the original class via its class loader. The instrument library allows applying the transformed bytecode to already loaded classes.
Before Java 6, instrumentation only worked at JVM startup; since then, it supports runtime class redefinition.
Usage
To use instrumentation, implement the ClassFileTransformer interface, whose transform() method receives the original bytecode and returns the modified version.
JVM TI
JVM TI (JVM Tool Interface) provides low‑level hooks into the JVM, enabling agents to monitor and modify heap, classes, threads, and more.
Agents are loaded via the -agentlib or -javaagent options, with jdwp acting as a bridge between JDI (high‑level debugging API) and JVM TI.
Agent
An agent is a native library (often written in C/C++) that is loaded at JVM startup or attached to a running JVM.
To attach an agent at runtime, the VirtualMachine class from tools.jar is used, requiring the target JVM's PID.
Usage
In the agent's premain() or agentmain() method, call Instrumentation.retransformClasses() to apply the transformed bytecode.
Code Implementation
The author provides a demo that modifies a simple class at runtime.
Target Class
public class TransformTarget {
public static void main(String[] args) {
while (true) {
try { Thread.sleep(3000L); } catch (Exception e) { break; }
printSomething();
}
}
public static void printSomething() {
System.out.println("hello");
}
}Agent
public class TestAgent {
public static void agentmain(String args, Instrumentation inst) {
inst.addTransformer(new TestTransformer(), true);
try {
inst.retransformClasses(TransformTarget.class);
System.out.println("Agent Load Done.");
} catch (Exception e) {
System.out.println("agent load failed!");
}
}
}Transformer
public class TestTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
System.out.println("Transforming " + className);
ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassVisitor visitor = new TestClassVisitor(Opcodes.ASM5, writer);
reader.accept(visitor, ClassReader.SKIP_DEBUG);
return writer.toByteArray();
}
class TestClassVisitor extends ClassVisitor implements Opcodes {
TestClassVisitor(int api, ClassVisitor cv) { super(api, cv); }
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if (name.equals("printSomething")) {
mv.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("bytecode replaced!");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(2, 0);
mv.visitEnd();
TransformTarget.printSomething();
}
return mv;
}
}
}Attacher
public class Attacher {
public static void main(String[] args) throws Exception {
VirtualMachine vm = VirtualMachine.attach("34242"); // target JVM pid
vm.loadAgent("/path/to/agent.jar");
}
}Running the target class, obtaining its PID, and using the attacher loads the agent, changing the output from "hello" to "bytecode replaced!".
Conclusion
Understanding dynamic bytecode modification clarifies Btrace's internals and opens the door to building custom Java performance and debugging tools.
The Java ecosystem offers a rich set of technologies for such low‑level manipulation, and mastering them enables developers to extend or adapt existing tools to their own needs.
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.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
