The Java Instrumentation API allows you to modify classes at runtime, providing capabilities for profiling, monitoring, and bytecode manipulation.
1. Overview and Basic Concepts
// Instrumentation API allows: // 1. Class transformation at loading time // 2. Retransformation of loaded classes // 3. Getting object sizes // 4. Redefining classes public class InstrumentationOverview { public static void main(String[] args) { System.out.println("Java Instrumentation API Key Features:"); System.out.println("1. Class File Transformation"); System.out.println("2. Bytecode Manipulation"); System.out.println("3. Runtime Monitoring"); System.out.println("4. Performance Profiling"); System.out.println("5. Dynamic Code Analysis"); } } 2. Basic Java Agent Structure
Premain Agent (Startup Time)
// SimpleAgent.java - Basic premain agent import java.lang.instrument.Instrumentation; public class SimpleAgent { /** * premain method called before main method */ public static void premain(String agentArgs, Instrumentation inst) { System.out.println("SimpleAgent premain method called"); System.out.println("Agent arguments: " + agentArgs); // Add class transformer inst.addTransformer(new SimpleClassTransformer()); // Display instrumentation capabilities System.out.println("Is retransform classes supported: " + inst.isRetransformClassesSupported()); System.out.println("Is redefine classes supported: " + inst.isRedefineClassesSupported()); System.out.println("Is native method prefix supported: " + inst.isNativeMethodPrefixSupported()); } /** * premain overload without Instrumentation */ public static void premain(String agentArgs) { System.out.println("SimpleAgent premain (no Instrumentation) called"); } } // MANIFEST.MF for premain agent // Manifest-Version: 1.0 // Premain-Class: SimpleAgent // Can-Redefine-Classes: true // Can-Retransform-Classes: true // Can-Set-Native-Method-Prefix: true Agentmain Agent (Runtime Attachment)
// DynamicAgent.java - Agent for runtime attachment import java.lang.instrument.Instrumentation; public class DynamicAgent { private static volatile Instrumentation globalInst; /** * agentmain method for runtime attachment */ public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("DynamicAgent agentmain method called"); globalInst = inst; // Add transformer for already loaded classes inst.addTransformer(new SimpleClassTransformer(), true); try { // Retransform all loaded classes Class[] allClasses = inst.getAllLoadedClasses(); for (Class clazz : allClasses) { if (inst.isModifiableClass(clazz) && !clazz.getName().startsWith("java.") && !clazz.getName().startsWith("sun.")) { inst.retransformClasses(clazz); } } } catch (Exception e) { e.printStackTrace(); } } public static Instrumentation getInstrumentation() { return globalInst; } } // MANIFEST.MF for agentmain // Manifest-Version: 1.0 // Agent-Class: DynamicAgent // Can-Redefine-Classes: true // Can-Retransform-Classes: true 3. Class File Transformer
Basic Class Transformer
// SimpleClassTransformer.java - Basic class file transformer import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; public class SimpleClassTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { // Filter system classes if (className == null || className.startsWith("java/") || className.startsWith("sun/") || className.startsWith("jdk/")) { return null; // null means no transformation } // Convert internal class name to normal format String normalizedClassName = className.replace('/', '.'); System.out.println("Transforming class: " + normalizedClassName); System.out.println(" ClassLoader: " + loader); System.out.println(" Buffer size: " + classfileBuffer.length + " bytes"); // You would typically modify classfileBuffer here // For this example, we just return the original bytes return classfileBuffer; // Return modified class bytes } } Method Timing Transformer
// MethodTimingTransformer.java - Adds timing to methods import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import javassist.*; public class MethodTimingTransformer implements ClassFileTransformer { private final String targetPackage; public MethodTimingTransformer(String targetPackage) { this.targetPackage = targetPackage; } @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (className == null) return null; String normalizedClassName = className.replace('/', '.'); // Only transform classes in target package if (!normalizedClassName.startsWith(targetPackage)) { return null; } try { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer)); // Skip interfaces and already modified classes if (ctClass.isInterface() || ctClass.getName().contains("$EnhancedBy")) { return null; } boolean modified = false; // Transform all methods for (CtMethod method : ctClass.getDeclaredMethods()) { if (!method.isEmpty() && !Modifier.isAbstract(method.getModifiers())) { addTimingToMethod(method); modified = true; } } if (modified) { System.out.println("Added timing to methods in: " + normalizedClassName); return ctClass.toBytecode(); } } catch (Exception e) { System.err.println("Failed to transform class: " + normalizedClassName); e.printStackTrace(); } return null; } private void addTimingToMethod(CtMethod method) throws CannotCompileException { String methodName = method.getName(); // Add local variable for start time method.addLocalVariable("__startTime", CtClass.longType); // Insert code at method start String startCode = "__startTime = System.nanoTime();"; method.insertBefore(startCode); // Insert code at method end (both normal return and exceptions) String endCode = String.format( "long __endTime = System.nanoTime();\n" + "System.out.printf(\"Method %s.%s executed in: %%d ns%n\", " + "(__endTime - __startTime));", method.getDeclaringClass().getSimpleName(), methodName ); method.insertAfter(endCode); } } 4. Bytecode Manipulation with ASM
// ASM-based Class Transformer import org.objectweb.asm.*; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; public class MethodLoggingTransformer implements ClassFileTransformer { private final String targetPackage; public MethodLoggingTransformer(String targetPackage) { this.targetPackage = targetPackage; } @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (className == null || !className.startsWith(targetPackage.replace('.', '/'))) { return null; } try { ClassReader classReader = new ClassReader(classfileBuffer); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassVisitor classVisitor = new MethodLoggingClassVisitor(classWriter, className); classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES); return classWriter.toByteArray(); } catch (Exception e) { System.err.println("ASM transformation failed for: " + className); e.printStackTrace(); return null; } } // Custom Class Visitor private static class MethodLoggingClassVisitor extends ClassVisitor { private String className; public MethodLoggingClassVisitor(ClassVisitor cv, String className) { super(Opcodes.ASM9, cv); this.className = className; } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); // Skip constructors, static initializers, and abstract methods if (mv != null && !name.equals("<init>") && !name.equals("<clinit>") && (access & Opcodes.ACC_ABSTRACT) == 0) { return new MethodLoggingMethodVisitor(mv, className, name, descriptor); } return mv; } } // Custom Method Visitor private static class MethodLoggingMethodVisitor extends MethodVisitor { private String className; private String methodName; private String methodDesc; public MethodLoggingMethodVisitor(MethodVisitor mv, String className, String methodName, String methodDesc) { super(Opcodes.ASM9, mv); this.className = className; this.methodName = methodName; this.methodDesc = methodDesc; } @Override public void visitCode() { // Insert logging at method start mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("ENTER: " + className + "." + methodName); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); super.visitCode(); } @Override public void visitInsn(int opcode) { // Insert logging before return instructions if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) { mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("EXIT: " + className + "." + methodName); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); } super.visitInsn(opcode); } } } 5. Advanced Instrumentation Agent
// AdvancedInstrumentationAgent.java - Comprehensive agent import java.lang.instrument.Instrumentation; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.UnmodifiableClassException; import java.util.*; public class AdvancedInstrumentationAgent { private static Instrumentation instrumentation; private static Map<String, ClassFileTransformer> transformers = new HashMap<>(); public static void premain(String agentArgs, Instrumentation inst) { agentmain(agentArgs, inst); } public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("AdvancedInstrumentationAgent initialized"); instrumentation = inst; // Parse agent arguments Properties args = parseArgs(agentArgs); // Register transformers based on arguments if ("true".equals(args.getProperty("enableTiming", "false"))) { registerTimingTransformer(); } if ("true".equals(args.getProperty("enableLogging", "false"))) { registerLoggingTransformer(); } if ("true".equals(args.getProperty("enableMonitoring", "false"))) { registerMonitoringTransformer(); } // Add shutdown hook for cleanup Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("Shutting down AdvancedInstrumentationAgent"); cleanup(); })); } private static Properties parseArgs(String agentArgs) { Properties props = new Properties(); if (agentArgs != null) { for (String arg : agentArgs.split(",")) { String[] keyValue = arg.split("="); if (keyValue.length == 2) { props.setProperty(keyValue[0].trim(), keyValue[1].trim()); } } } return props; } private static void registerTimingTransformer() { String packageName = "com.example"; // Target package MethodTimingTransformer transformer = new MethodTimingTransformer(packageName); instrumentation.addTransformer(transformer); transformers.put("timing", transformer); System.out.println("Registered MethodTimingTransformer for package: " + packageName); } private static void registerLoggingTransformer() { String packageName = "com.example"; MethodLoggingTransformer transformer = new MethodLoggingTransformer(packageName); instrumentation.addTransformer(transformer, true); transformers.put("logging", transformer); System.out.println("Registered MethodLoggingTransformer for package: " + packageName); // Retransform already loaded classes retransformLoadedClasses(packageName); } private static void registerMonitoringTransformer() { // Custom monitoring transformer ClassFileTransformer transformer = new MonitoringTransformer(); instrumentation.addTransformer(transformer); transformers.put("monitoring", transformer); System.out.println("Registered MonitoringTransformer"); } private static void retransformLoadedClasses(String packageName) { try { Class[] loadedClasses = instrumentation.getAllLoadedClasses(); List<Class> toRetransform = new ArrayList<>(); for (Class clazz : loadedClasses) { if (clazz != null && instrumentation.isModifiableClass(clazz) && clazz.getName().startsWith(packageName) && !clazz.getName().contains("$")) { toRetransform.add(clazz); } } if (!toRetransform.isEmpty()) { System.out.println("Retransforming " + toRetransform.size() + " loaded classes"); instrumentation.retransformClasses(toRetransform.toArray(new Class[0])); } } catch (UnmodifiableClassException e) { System.err.println("Failed to retransform classes: " + e.getMessage()); } } public static void cleanup() { for (ClassFileTransformer transformer : transformers.values()) { instrumentation.removeTransformer(transformer); } transformers.clear(); } // Utility methods public static long getObjectSize(Object obj) { if (instrumentation != null) { return instrumentation.getObjectSize(obj); } return -1; } public static Class[] getAllLoadedClasses() { if (instrumentation != null) { return instrumentation.getAllLoadedClasses(); } return new Class[0]; } public static void redefineClass(Class<?> clazz, byte[] bytecode) throws Exception { if (instrumentation != null) { instrumentation.redefineClasses(new java.lang.instrument.ClassDefinition(clazz, bytecode)); } } } // Monitoring Transformer class MonitoringTransformer implements ClassFileTransformer { private Map<String, Integer> transformationCount = new ConcurrentHashMap<>(); @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { if (className != null && !className.startsWith("java/")) { transformationCount.merge(className, 1, Integer::sum); if (transformationCount.get(className) == 1) { System.out.println("First transformation: " + className); } } return null; // No actual transformation } public Map<String, Integer> getTransformationStats() { return new HashMap<>(transformationCount); } } 6. Building and Packaging Agents
Maven Configuration
<!-- pom.xml for building instrumentation agent --> <project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>java-instrumentation-agent</artifactId> <version>1.0.0</version> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- ASM for bytecode manipulation --> <dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> <version>9.4</version> </dependency> <dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm-commons</artifactId> <version>9.4</version> </dependency> <!-- Javassist for bytecode manipulation --> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.29.2-GA</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.2</version> <configuration> <archive> <manifestEntries> <Premain-Class>AdvancedInstrumentationAgent</Premain-Class> <Agent-Class>AdvancedInstrumentationAgent</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix> </manifestEntries> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
Build Script
#!/bin/bash # build-agent.sh echo "Building Java Instrumentation Agent..." # Compile Java files javac -cp ".:lib/*" -d build src/*.java # Create manifest echo "Manifest-Version: 1.0" > MANIFEST.MF echo "Premain-Class: AdvancedInstrumentationAgent" >> MANIFEST.MF echo "Agent-Class: AdvancedInstrumentationAgent" >> MANIFEST.MF echo "Can-Redefine-Classes: true" >> MANIFEST.MF echo "Can-Retransform-Classes: true" >> MANIFEST.MF echo "Can-Set-Native-Method-Prefix: true" >> MANIFEST.MF echo "Class-Path: lib/asm-9.4.jar lib/asm-commons-9.4.jar lib/javassist-3.29.2-GA.jar" >> MANIFEST.MF # Create JAR jar cfm agent.jar MANIFEST.MF -C build . echo "Agent built: agent.jar"
7. Using the Instrumentation Agent
Test Application
// TestApplication.java - Application to be instrumented package com.example; public class TestApplication { public static void main(String[] args) { System.out.println("TestApplication started"); TestApplication app = new TestApplication(); app.runBusinessLogic(); System.out.println("TestApplication completed"); } public void runBusinessLogic() { processData("Sample Data", 100); calculateResults(5, 3); performComplexOperation(); } private void processData(String data, int count) { System.out.println("Processing data: " + data + ", count: " + count); try { Thread.sleep(100); // Simulate work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private int calculateResults(int a, int b) { int result = a * b + a + b; System.out.println("Calculated result: " + result); return result; } private void performComplexOperation() { System.out.println("Performing complex operation..."); for (int i = 0; i < 3; i++) { processData("Iteration " + i, i); } } } Running with Premain Agent
# Run with premain agent java -javaagent:agent.jar=enableTiming=true,enableLogging=true -cp . com.example.TestApplication # Run with specific package targeting java -javaagent:agent.jar=enableTiming=true,targetPackage=com.example -cp . com.example.TestApplication
Runtime Attachment
// RuntimeAttacher.java - Attach agent to running JVM import com.sun.tools.attach.*; public class RuntimeAttacher { public static void attachAgent(String pid, String agentJarPath, String options) { try { VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(agentJarPath, options); vm.detach(); System.out.println("Agent attached successfully to PID: " + pid); } catch (Exception e) { System.err.println("Failed to attach agent: " + e.getMessage()); e.printStackTrace(); } } public static void listJavaProcesses() { System.out.println("Available Java processes:"); for (VirtualMachineDescriptor vmd : VirtualMachine.list()) { System.out.println("PID: " + vmd.id() + " - " + vmd.displayName()); } } public static void main(String[] args) { if (args.length == 0) { listJavaProcesses(); return; } String pid = args[0]; String agentJar = "agent.jar"; String options = "enableTiming=true,enableLogging=true"; attachAgent(pid, agentJar, options); } } 8. Practical Use Cases
Memory Usage Monitoring
// MemoryMonitoringAgent.java - Monitor object allocations import java.lang.instrument.Instrumentation; import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; import java.util.concurrent.ConcurrentHashMap; import java.util.Map; public class MemoryMonitoringAgent { private static Instrumentation instrumentation; private static Map<Class<?>, Long> objectCounts = new ConcurrentHashMap<>(); private static Map<Class<?>, Long> totalSize = new ConcurrentHashMap<>(); public static void premain(String args, Instrumentation inst) { instrumentation = inst; inst.addTransformer(new AllocationTransformer()); } public static void printMemoryStats() { System.out.println("\n=== Memory Usage Statistics ==="); objectCounts.forEach((clazz, count) -> { long size = totalSize.getOrDefault(clazz, 0L); System.out.printf("%s: count=%d, totalSize=%d bytes, avgSize=%.2f bytes%n", clazz.getSimpleName(), count, size, (double)size / count); }); } static class AllocationTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { // In a real implementation, you would modify the bytecode // to track object allocations return null; } } // Called from instrumented code public static void recordAllocation(Class<?> clazz, long size) { objectCounts.merge(clazz, 1L, Long::sum); totalSize.merge(clazz, size, Long::sum); } public static long getObjectSize(Object obj) { return instrumentation.getObjectSize(obj); } } Key Instrumentation API Features
- Class Transformation: Modify classes as they're loaded
- Retransformation: Modify already loaded classes
- Class Redefinition: Replace class definitions at runtime
- Object Size Measurement: Get shallow size of objects
- Native Method Prefixing: Intercept native method calls
Common Use Cases
- APM Tools: Application performance monitoring
- Profiling: Method timing, memory usage
- Debugging: Add logging, breakpoints
- Security: Access control, vulnerability detection
- Testing: Code coverage, mutation testing
- Hot-patching: Fix bugs in running applications
The Instrumentation API is powerful but requires careful use as it can significantly impact application performance and stability.