In-case you haven’t read Part 1, you can find it here.

Introduction

Let’s look at the Java Pyramid program we will be profiling. We will assume we do not have access to the source code. The program gives us the following output.

   1
  1 1
 1 1 1
1 1 1 1
    1
   1 1
  1 1 1
 1 1 1 1
1 1 1 1 1
     1
    1 1
   1 1 1
  1 1 1 1
 1 1 1 1 1
1 1 1 1 1 1

On running our profiler with the Java Pyramid program we get the following metrics

========= Method Count Metrics =========
com.oliver.printpyramid.PrintPyramid.printPyramid-->3
com.oliver.printpyramid.PrintPyramid.main-->1
com.oliver.printpyramid.PrintPyramid.getLine-->15
========= End Method Count Metrics =========

Now we have some insight into which methods are being called by the Pyramid program.

This basic idea can be extended to record other metrics like method execution time, the results of which can be used to ease out performance bottlenecks.

Motivation

Java development tools like JRebel and XRebel as well as Application Monitoring tools like New Relic APM are built upon similar albeit more complicated concepts of Java agents and byte-code instrumentation.

Let’s Begin

In sections below will look at:

  1. A quick overview of Javassist.
  2. Define the Metrics Collector Interface.
  3. Implement and Register a custom Class File Transformer.
  4. Generate a Fat Jar i.e. a jar with dependencies.

A quick overview of Javassist

Javassist is a class library for editing bytecodes in Java; it enables Java programs to define a new class at runtime and to modify a class file when the JVM loads it.

Javassist Components Image Source

The main components of Javassist as indicated by the image above consist of

  • CtClass
  • ClassPool
  • CtMethod

CtClass

A CtClass object is an abstraction of a class file. In order to manipulate the byte-code of a class we will have to obtain its CtClass representation from the ClassPool.

ClassPool

The ClassPool object holds multiple CtClass objects.

CtMethod

A CtMethod object is an abstraction of a method in a class. Once we obtain the CtMethod representation of a method we can manipulate its byte-code using the various methods present. In this example we will use the insertBefore method to add our instrumentation code.

Note: For a more complete and detailed explanation on Javassist please see references 2 and 3.

The Metrics Collector Interface

Here is the MetricsCollector interface which will be instrumented into the Java Pyramid program. The implementation of this interface is pretty straightforward and can be followed through with just the javadoc comments.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.oliver.jagent.mectrics;

public interface MetricsCollector {

	/**
	 * This method is invoked at the beginning of every method call, it increments
	 * the counter associated with the the <code>methodName</code> param.
	 * 
	 * @param className
	 * @param methodName
	 */
	public void logMethodCall(String className, String methodName);

	/**
	 * This method registers the names of the methods which will invoke
	 * logMethodCall() on every call.
	 * 
	 * @param className
	 * @param methodName
	 */
	public void registerMethod(String className, String methodName);

	/**
	 * Prints out the recorded Metric values
	 * 
	 * @return
	 */
	public String printMetrics();
}
  • registerMethod() will be invoked by our agent code to register all the method names that will be instrumented.
  • logMethodCall() will be instrumented into the Java Pyramid program code via byte-code modification so that it is invoked every-time a method of the Java Pyramid program is invoked.
  • printMetrics() will be invoked by our agent code on shut-down and will print out the metrics.

Registering a custom Class File Transformer

In the first post we implemented the premain method and displayed a “Hello World! Java Agent” message. In order to modify byte-code we need to write and register a custom ClassFileTransformer. This is done by using the addTransformer method of the java.lang.instrument.Instrumentation interface.

1
2
3
4
5
6
7
package com.oliver.jagent;

import java.lang.instrument.Instrumentation;

public static void premain(String agentArgs, Instrumentation inst){
		inst.addTransformer(new MyClassTransformer());
}

Implementing MyClassTransformer

The MyClassTransformer class implements the ClassFileTransformer interface and the transform method. We have initialized a ClassPool with the default class pool, this is fine when running a simple application like the Java Pyramid program in this example. However for applications running on web application servers like Tomcat and JBoss which use multiple class loaders, creating multiple instances of ClassPool might be necessary; an instance of ClassPool should be created for each class loader. You may find mode details on this here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import javassist.ClassPool;

public class MyClassTransformer implements ClassFileTransformer {

  private ClassPool pool;

  public MyClassTransformer() {
	this.pool = ClassPool.getDefault();
  }	

  public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
	ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
	...
	}
}			

Filtering out Classes we do not intent to modify

Our agent code will intercept all the classes to be loaded by the VM including the its own classes, hence we need to filter out these classes in the transformer and return their byte-code without any modification. The following code snippet does just this.

1
2
3
4
5
6
7
8
9
10
byte[] modifiedByteCode = classfileBuffer;
String clazzName = className.replace("/", ".");
//Skip all agent classes
if (clazzName.startsWith("com.oliver.jagent")) {
	return classfileBuffer;
}
//Skip class if it doesn't belong to our Java Pyramid program
if (!clazzName.startsWith("com.oliver.printpyramid")) {
	return classfileBuffer;
}

Adding the instrumentation code

Now that we have filtered out the unwanted classes it’s time to add our instrumentation code. The following code snippet will obtain a class representation from the ClassPool, iterate over all the methods presents in the class and use insertBefore() to add our instrumentation code at the start of every method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private MetricsCollector collector = MetricsCollectorImpl.instance();
...
//Records a package name so that the Javassist compiler may resolve a class name.
//This enables the compiled to resolve the class "MetricsCollectorImpl" below
pool.importPackage("com.oliver.jagent.mectrics");
//Retrieve the class representation i.e. CtClass object
CtClass cclass = pool.get(clazzName);
		
for (CtMethod method : cclass.getDeclaredMethods()) {
	//Register the method with our MetricsCollector
	collector.registerMethod(clazzName, method.getName());
	//Insert the instrumentation code at the start of the method.
	method.insertBefore("MetricsCollectorImpl.instance().logMethodCall(\"" + clazzName + "\",\"" + method.getName() + "\");");
}

Note: Exception handling has been omitted for brevity.

Generating a Fat JAR

In this example, we have used Javassist which is a third-party library. When running this version of the agent you will need to specify a path to this jar as well. In order to keep things simple, tools like JRebel create a “fat jar” or “jar with dependencies”, which uses a single JAR file. This can be achieved using the maven-assembly-plugin.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-assembly-plugin</artifactId>
	<version>2.6</version>
	<configuration>
	  <descriptorRefs>
	    <descriptorRef>jar-with-dependencies</descriptorRef>
	  </descriptorRefs>
	  <archive>
	    <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
	  </archive>
	</configuration>
	<executions>
	  <execution>
	    <id>assemble-all</id>
	    <phase>package</phase>
	    <goals>
		 <goal>single</goal>
	    </goals>
	  </execution>
	</executions>
</plugin>

Similar to Part 1, <manifestFile> contains the path to our custom manifest file, no changes have been made there except a change in the agent class name.

1
2
Manifest-Version: 1.0
Premain-Class: com.oliver.jagent.Agent

The agent is run in exactly the same way as described in Part 1.

Conclusion

We have created a basic java profiler which gives us insights into the inner workings of applications and have run the profiler as a java agent.

References

  1. Java Instrumentation Package
  2. Javassist
  3. Diving into Bytecode Manipulation: Creating an Audit Log with ASM and Javassist.
  4. Callspy - A simple trace agent.