Contents

A .class file has a fixed binary structure defined by the JVM specification:

The JVM is a stack-based virtual machine. Instructions push and pop operands on a per-frame operand stack rather than using registers like x86/ARM.

javap is the JDK disassembler. Use -c for bytecode and -v (verbose) for the full constant pool and metadata.

// Source code public class Counter { private int count = 0; public int increment() { return ++count; } } # Compile then disassemble javac Counter.java javap -c Counter // Disassembled output for increment() public int increment(); Code: 0: aload_0 // push 'this' onto operand stack 1: dup // duplicate top of stack 2: getfield #7 // pop 'this', push this.count (int) 5: iconst_1 // push constant 1 6: iadd // pop count and 1, push count+1 7: putfield #7 // pop 'this' and new value, store to this.count 10: aload_0 // push 'this' again 11: getfield #7 // push this.count (now incremented) 14: ireturn // return int at top of stack # Full verbose output (constant pool, line number table, stack frames) javap -v Counter

There are ~200 opcodes. These are the most frequently encountered:

// Load/store aload_0 load reference from local variable 0 (usually 'this') iload_1 load int from local variable 1 astore_2 store reference to local variable 2 // Constants iconst_0..5 push int constant 0-5 ldc #n push constant from constant pool (String, int, float, Class) aconst_null push null // Arithmetic iadd / isub / imul / idiv / irem ladd / lsub (long variants) fadd / dadd (float/double variants) // Object/field/method new #n allocate new object (does NOT call constructor) invokespecial call constructor or private/super method invokevirtual polymorphic instance method dispatch invokestatic static method invokeinterface interface method call invokedynamic lambda / string concatenation (since Java 7) getfield / putfield instance field access getstatic / putstatic static field access // Control flow if_icmpeq/ne/lt/ge/gt/le int comparison branches goto #offset unconditional jump tableswitch dense switch lookupswitch sparse switch // Return ireturn / lreturn / freturn / dreturn / areturn / return (void) // Exception athrow throw exception (object on stack)

A Java agent is a JAR that the JVM loads before main(). It receives an Instrumentation handle that lets it add ClassFileTransformers.

// Agent entry point — called before main() import java.lang.instrument.Instrumentation; public class TimingAgent { public static void premain(String agentArgs, Instrumentation inst) { System.out.println("[Agent] Loaded with args: " + agentArgs); inst.addTransformer(new TimingTransformer(), /* canRetransform= */ true); } }

The agent JAR's MANIFEST.MF must declare the entry point:

Manifest-Version: 1.0 Premain-Class: com.example.TimingAgent Agent-Class: com.example.TimingAgent Can-Retransform-Classes: true Can-Redefine-Classes: true # Attach agent at JVM startup java -javaagent:timing-agent.jar=verbose myapp.Main

A ClassFileTransformer receives raw bytecode bytes for every class being loaded and can return modified bytes. The example below uses ASM (the industry-standard bytecode library) to inject a timing print around every method entry.

import java.lang.instrument.*; import java.security.ProtectionDomain; import org.objectweb.asm.*; public class TimingTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain pd, byte[] classfileBuffer) { // Skip JDK internals and the agent itself if (className == null || className.startsWith("java/") || className.startsWith("sun/") || className.startsWith("com/example/agent")) { return null; // null = no transformation } try { ClassReader cr = new ClassReader(classfileBuffer); ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES); ClassVisitor cv = new TimingClassVisitor(cw, className); cr.accept(cv, ClassReader.EXPAND_FRAMES); return cw.toByteArray(); } catch (Exception e) { e.printStackTrace(); return null; // return null on error to keep original bytes } } } class TimingClassVisitor extends ClassVisitor { private final String className; TimingClassVisitor(ClassVisitor cv, String name) { super(Opcodes.ASM9, cv); this.className = name; } @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("<init>") || name.equals("<clinit>")) return mv; return new TimingMethodVisitor(mv, className + "." + name); } } class TimingMethodVisitor extends MethodVisitor { private final String methodName; TimingMethodVisitor(MethodVisitor mv, String name) { super(Opcodes.ASM9, mv); this.methodName = name; } @Override public void visitCode() { // Inject: System.out.println("[ENTER] methodName"); mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("[ENTER] " + methodName); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); super.visitCode(); } } Always use ClassWriter.COMPUTE_FRAMES when modifying bytecode — computing stack map frames by hand is error-prone and version-dependent.

Agents can also be attached to a running JVM without restarting it — useful for profilers and diagnostic tools.

// In the agent JAR — agentmain is called for dynamic attach public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("[Agent] Dynamically attached!"); // retransform already-loaded classes for (Class<?> clazz : inst.getAllLoadedClasses()) { if (inst.isModifiableClass(clazz) && clazz.getName().startsWith("com.myapp")) { try { inst.retransformClasses(clazz); } catch (UnmodifiableClassException e) { // some JDK-internal classes cannot be retransformed } } } } // Attach from another JVM process (requires tools.jar / JDK) import com.sun.tools.attach.VirtualMachine; VirtualMachine vm = VirtualMachine.attach("12345"); // target PID vm.loadAgent("/path/to/agent.jar", "optionalArgs"); vm.detach();

Working directly with ASM opcodes is verbose. Byte Buddy provides a fluent, type-safe API for common transformation patterns, used internally by Mockito, Hibernate, and many APM agents.

// Maven: net.bytebuddy:byte-buddy:1.14.x import net.bytebuddy.ByteBuddy; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.matcher.ElementMatchers; // 1. Subclass & method interception (no agent needed) Class<? extends Greeter> dynamic = new ByteBuddy() .subclass(Greeter.class) .method(ElementMatchers.named("sayHello")) .intercept(Advice.to(LoggingAdvice.class)) .make() .load(Greeter.class.getClassLoader()) .getLoaded(); Greeter instance = dynamic.getDeclaredConstructor().newInstance(); instance.sayHello("World"); // 2. The @Advice class — defines enter/exit interceptors class LoggingAdvice { @Advice.OnMethodEnter static long enter(@Advice.Origin String method) { System.out.println("[ENTER] " + method); return System.nanoTime(); } @Advice.OnMethodExit(onThrowable = Throwable.class) static void exit(@Advice.Origin String method, @Advice.Enter long startNs, @Advice.Thrown Throwable thrown) { long elapsedMs = (System.nanoTime() - startNs) / 1_000_000; System.out.printf("[EXIT] %s — %d ms%n", method, elapsedMs); if (thrown != null) System.out.println("[THROW] " + thrown); } } // 3. Use Byte Buddy inside a Java agent for load-time weaving public static void premain(String args, Instrumentation inst) { new AgentBuilder.Default() .type(ElementMatchers.nameStartsWith("com.myapp")) .transform((builder, type, loader, module, pd) -> builder.method(ElementMatchers.any()) .intercept(Advice.to(LoggingAdvice.class))) .installOn(inst); }
For production instrumentation, prefer Byte Buddy's AgentBuilder over raw ASM. It handles retransformation, module access, and Java version compatibility automatically.