Java Instrumentation

Java Instrumentation will give a demonstration of how powerful Java is. Most importantly, this power can be realized by a developer for innovative means. For example using Java instrumentation, we can access a class that is loaded by the Java classloader from the JVM and modify its bytecode by inserting our custom code, all these done at runtime. Don’t worry about security, these are governed by the same security context applicable for Java classes and respective classloaders.

With this tutorial let us learn to instrument Java byte code using Java instrumentation. Mostly profilers, application monitoring agents, event loggers use Java instrumentation. This will serve as introductory level tutorial and once it is done, you can write a basic Java agent and do instrumentation on the Java byte code.

Key Components of Java Instrumentation

  • Agent – is a jar file containing agent and transformer class files.
  • Agent Class – A java class file, containing a method named ‘premain’.
  • Manifest – manifest.mf file containing the “premain-class” property.
  • Transformer – A Java class file implementing the interface ClassFileTransformer

Instrumentation Agent Class

Agent class contains the premain method and that is key in Java insturmentation. This is similar to the ‘main’ method. This class is loaded by the same system classloader as it loads the other Java classes. premain method can have the following signatures,
  1. public static void premain(String agentArgs, Instrumentation inst);
  2. public static void premain(String agentArgs); 
In general, System classloader after loading the Agent Class, then invokes the premain (premain is roughly equal to main method for normal Java classes) method in the above given order. Either 1 or 2 must be present in the class. Initiating the tool’s agent in Java instrumentation is JVM implementation dependent, so you got to read the implementation specs. premain method runs under the same security context as the application main method.
  • Similar to passing arguments to main method of an application, we can pass arguments to agent via agentArgs parameter. The difference is main method accepts array as parameter, but premain accepts only a String. May be we should find our own way, like send a long string and parse it inside the code to pass multiple parameters.
  • If the premain method throws an exception and it goes uncaught then the JVM will abort instantly.
  • instrumentation is an instance passed by the classloader using which we can register our transformers.

Instrumentation Transformer

Transformer classes are to be registered with ‘instrumentation’ instance. All those transformers that are registered with instrumentation instance will be invoked by the classloader every time a new class is loaded by that. ClassFileTransformer has got a method with following signature, which must be implemented,
  • public byte[] transform(ClassLoader loader, String className,
    Class classBeingRedefined,
    ProtectionDomain  protectionDomain, 
    byte[] classfileBuffer) throws llegalClassFormatException
When the transformer is invoked by the classloader, it will pass all these arguments, on which class loading this is invoked, etc and the bytecode of the class being loaded. So that’s our entry ticket to manipulating the class loaded. We can take a copy of the classfileBuffer and make modifications to the copy and return it. So the returned bytes are loaded by the classloader which will include the modifications we have done.

Instrumentation Activity Sequence

Following diagram summarizes the activity flow in Java instrumentation,


Example for Java Instrumentation

In this example, we will use Java instrumentation to modify the loaded bytecode (java classes) and add statements. Using which we will find out how much duration a method executes. This is a real time use case for java performance profiling.

DurationTransformer.java

This is the Java class that will be registered with the Java instrumentation agent. This will modify the class’ bytecode by inserting new lines and the original class will be replaced by the classloader.

package com.javapapers.java.instrumentation;
import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
//this class will be registered with instrumentation agent
public class DurationTransformer implements ClassFileTransformer {
  public byte[] transform(ClassLoader loader, String className,
      Class classBeingRedefined, ProtectionDomain protectionDomain,
      byte[] classfileBuffer) throws IllegalClassFormatException {
    byte[] byteCode = classfileBuffer;
    // since this transformer will be called when all the classes are
    // loaded by the classloader, we are restricting the instrumentation
    // using if block only for the Lion class
    if (className.equals("com/javapapers/java/instrumentation/Lion")) {
      System.out.println("Instrumenting......");
      try {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(
            classfileBuffer));
        CtMethod[] methods = ctClass.getDeclaredMethods();
        for (CtMethod method : methods) {
          method.addLocalVariable("startTime", CtClass.longType);
          method.insertBefore("startTime = System.nanoTime();");
          method.insertAfter("System.out.println(\"Execution Duration "
              + "(nano sec): \"+ (System.nanoTime() - startTime) );");
        }
        byteCode = ctClass.toBytecode();
        ctClass.detach();
        System.out.println("Instrumentation complete.");
      } catch (Throwable ex) {
        System.out.println("Exception: " + ex);
        ex.printStackTrace();
      }
    }
    return byteCode;
  }
}

DurationAgent.java

package java.instrumentation;
import java.lang.instrument.Instrumentation;
public class DurationAgent {
  public static void premain(String agentArgs, Instrumentation inst) {
    System.out.println("Executing premain.........");
    inst.addTransformer(new DurationTransformer());
  }
}

Lion.java

package java.instrumentation;
//to be instrumented java class
public class Lion {
  public void runLion() throws InterruptedException {
    System.out.println("Lion is going to run........");
    Thread.sleep(2000L);
  }
}

TestInstrumentation.java

package java.instrumentation;
public class TestInstrumentation {
  public static void main(String args[]) throws InterruptedException {
    Lion l = new Lion();
    l.runLion();
  }
}

Java Instrumentation Project Execution

Execution of this program is not done as usual and you have to follow the steps as below
  1. Compile the program and prepare the Java Jar file. This is the agent jar.
  2. Pass the agent jar prepared for instrumentation using -javaagent argument while the execution of the program.
  • You can download the Java instrumentation sample project and it is an Eclipse project.
  • Build it using Eclipse and you will get the class files.
  • You may run the ‘jar’ command from command line and prepare the agent jar.jar cvfm instrumentation.jar ..\meta-inf\manifest.mf . Run this from the bin folder.
  • Execute the Java instrumentation project using command, java -cp .;./lib/javassist-3.14.0-GA.jar -javaagent:./instrumentation.jar com.javapapers.java.instrumentation.TestInstrumentation. Run it from the instrumentation Java project root.

Output of the Java instrumentation Project

Executing premain………
Instrumenting……
Instrumentation complete.
Lion is going to run……..
Execution Duration (nano sec): 2000755165

The line ‘Execution Duration’ is what is inserted by the Java instrumentation and this line is not present in the original Java code

 

 




 



 

 

0 comments:

Post a Comment