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.