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:
- A quick overview of
Javassist
. - Define the Metrics Collector Interface.
- Implement and Register a custom Class File Transformer.
- 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.
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.