 | Level: Intermediate Dennis Sosnoski (dms@sosnoski.com), President, Sosnoski Software Solutions, Inc.
12 May 2005 In this edition of Classworking toolkit, consultant Dennis Sosnoski compares the ASM bytecode manipulation framework to the Byte Code Engineering Library (BCEL) and Javassist frameworks he previously discussed in his Java programming dynamics series. ASM claims to be small and fast -- but how does it match up with the other frameworks? Dennis uses an example from his earlier series to evaluate both usability and performance.
Several Java™ libraries have been developed for working with bytecode and
classfiles, including the Javassist and BCEL libraries that I covered in my
earlier series on Java programming dynamics (see Resources). ASM is another, more recent library of this
type. Unlike the other libraries, ASM was designed and implemented to be as
small and fast as possible. In this month's column, I'll look into how
well ASM delivers on this intent, comparing it to the other two libraries for an
example I used in the series.
In the earlier article, I demonstrated how run-time bytecode generation can be
used to replace reflection. At that time, I tested with a 1.4.1 JVM and found
that the generated code could run much faster than the reflection code
it replaced. Besides trying the same approach with ASM, I'll also update the
results in this column to use a 1.5.0 JVM to see if the performance enhancements
implemented in 1.5.0 have changed the results.
Replacing reflection
The purpose of the example application is to replace reflection with code
generated at run time. I covered this topic in depth in my Java
programming dynamics series. For
this column, I'll give a quick background summary based on the earlier material and then look at using ASM as an alternative to the Javassist and BCEL frameworks to see how both the performance and usability of ASM compare to the others.
Setting the stage
Reflection provides a very powerful mechanism for accessing both objects and
metadata at run time (as I discussed in "Java
programming dynamics, Part 2"). Using reflection allows applications to be
structured for flexibility, with external information used at run time to
hook pieces together into a working configuration. But when used for actual
object access, reflection is generally much slower than performing the same
operation directly. Building an application around a reflection-based
approach and then finding you need to upgrade performance can create a real
problem, because the flexibility allowed by reflection can be hard to duplicate in
other ways.
Classworking techniques offer a way out. Rather than using reflection to
access a property of an object, for example, you can construct a class at
run time that does the same thing -- but does it much faster. "Java
programming dynamics, Part 8" demonstrates how to implement this type
of reflection replacement using both the Javassist and BCEL classworking
frameworks. The basic principle that article uses is simple: First create an
interface that defines the functions you need, then build a class at run time
that implements the interface and hooks the functions to a target object.
Listing 1 demonstrates this approach. Here the HolderBean class contains a pair of properties that can
be accessed at run time by using reflection to call the get and set methods. The
IAccess interface abstracts the idea of accessing an
int-valued property through get and set methods, and
the AccessValue1 class gives an implementation of
this interface specifically for the "value1" property of the HolderBean class.
Listing 1. Reflection-replacement interface and implementation
public class HolderBean
{
private int m_value1;
private int m_value2;
public int getValue1() {
return m_value1;
}
public void setValue1(int value) {
m_value1 = value;
}
public int getValue2() {
return m_value2;
}
public void setValue2(int value) {
m_value2 = value;
}
}
public interface IAccess
{
public void setTarget(Object target);
public int getValue();
public void setValue(int value);
}
public class AccessValue1 implements IAccess
{
private HolderBean m_target;
public void setTarget(Object target) {
m_target = (HolderBean)target;
}
public int getValue() {
return m_target.getValue1();
}
public void setValue(int value) {
m_target.setValue1(value);
}
}
|
If you had to hand-code each implementation class like AccessValue1 in Listing 1, this whole
approach wouldn't be very useful. But the code in AccessValue1 is very simple, making it an ideal target for
run-time class generation. You can use the AccessValue1 bytecode as a template for generating a class
that's specific to a particular target object type and get/set method pair, just
substituting these targets in place of those used in AccessValue1. That's the approach I use in the earlier
article, and it's the same one I'm applying with ASM in this column.
Working with ASM
The two classworking frameworks I cover in my earlier articles take very
different approaches to working with bytecode. Javassist uses a simplified
version of Java source code, which it then compiles into bytecode. That makes
Javassist very easy to use, but it also limits the use of bytecode to what can be
expressed within the limits of the Javassist source code. BCEL, on the other
hand, works directly with bytecode. BCEL provides structures and techniques for
manipulating bytecode instructions that bring it up a step from the level of
pure binary values, but it is much harder to work with than
Javassist.
ASM is closer to BCEL than to Javassist in terms of the level of operations,
but ASM uses an interface that I find much cleaner than BCEL's. One reason for this is because of the basic design of ASM. Rather than manipulating bytecode instructions
directly, ASM uses a visitor pattern to process class data (including
instruction sequences) as streams of events. When decoding an existing class, ASM
generates the stream of events for you, calling your methods for processing the
events. When generating a new class, this approach is turned around -- you make
the calls to an ASM class, and it builds the new class from the stream of events
represented by the calls. You can also use the two sides together, intercepting
the stream of events generated from an existing class to make some changes and
feeding the altered stream back into the generation of a new class.
ASMifying a class
Both BCEL and ASM come equipped with tools that generate the Java source
code to write a class. The idea behind these tools is that you can use an
existing class as the template for your run-time class generation. The generated
source code contains all the calls necessary to reproduce the binary form of
your template class, so you can theoretically merge this code into your
application code and modify it to suit your needs (such as by substituting in
parameters for values that need to be modifiable at run time).
In practice, I found the BCEL version of this class writing program (org.apache.bcel.util.BCELifier) to be of limited use. The BCEL
code for manipulating lists of instructions is complex, and to me the source
code generated by BCELifier was too ugly to be
usable. The ASM class writing program also produces some ugly looking code, but
with some minor cleanup it does seem useful. Listing 2 shows
the results from running this program (org.objectweb.asm.util.ASMifierClassVisitor) on the gen.AccessValue1 class from Listing
1.
Listing 2. ASM code generated from gen.AccessValue1
package asm.gen;
import org.objectweb.asm.*;
public class AccessValue1Dump implements Opcodes {
public static byte[] dump () throws Exception {
ClassWriter cw = new ClassWriter(false);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(V1_2, ACC_PUBLIC + ACC_SUPER, "gen/AccessValue1", null, "java/lang/Object", new String[]
{ "gen/IAccess" });
cw.visitSource("AccessValue1.java", null);
{
fv = cw.visitField(0, "m_bean", "Lgen/HolderBean;", null, null);
fv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "setTarget", "(Ljava/lang/Object;)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitTypeInsn(CHECKCAST, "gen/HolderBean");
mv.visitFieldInsn(PUTFIELD, "gen/AccessValue1", "m_bean", "Lgen/HolderBean;");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "getValue", "()I", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "gen/AccessValue1", "m_bean", "Lgen/HolderBean;");
mv.visitMethodInsn(INVOKEVIRTUAL, "gen/HolderBean", "getValue1", "()I");
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "setValue", "(I)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "gen/AccessValue1", "m_bean", "Lgen/HolderBean;");
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKEVIRTUAL, "gen/HolderBean", "setValue1", "(I)V");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}
|
With some reformatting, the Listing 2 code is the
basis for the reflection replacement code we'll look at next.
Replacing reflection with ASM
"Java
programming dynamics, Part 8" uses a base class for testing different
implementations of code generation to replace reflection, where each
classworking library uses a separate subclass extending the base class. I'll use
this same approach for trying out ASM.
Listing 3 gives the ASM implementation subclass. The
construction of the reflection replacement class is done using the createAccess() method, which is based on the ASM-generated
code from Listing 2. The main differences from the Listing 2
code are that I reformatted and restructured Listing 3 a bit, and I have also made
parameters of the target class, property get and set methods, and generated
class name, so that this ASM version of the createAccess() method is compatible with the Javassist and
BCEL versions used in the earlier article.
Listing 3. ASM test class
public class ASMCalls extends TimeCalls
{
protected byte[] createAccess(Class tclas, Method gmeth, Method smeth,
String cname) throws Exception {
// initialize writer for new class
String ciname = cname.replace('.', '/');
ClassWriter cw = new ClassWriter(false);
cw.visit(Opcodes.V1_2, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER,
cname, null, "java/lang/Object", new String[] { "gen/IAccess" });
// add field definition for reference to target class instance
String tiname = Type.getInternalName(tclas);
String ttype = "L" + tiname + ";";
cw.visitField(0, "m_bean", ttype, null, null).visitEnd();
// generate the default constructor
MethodVisitor mv =
cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
"<init>", "()V");
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
// generate the setTarget method
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "setTarget",
"(Ljava/lang/Object;)V", null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitTypeInsn(Opcodes.CHECKCAST, tiname);
mv.visitFieldInsn(Opcodes.PUTFIELD, ciname, "m_bean", ttype);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
// generate the getValue method
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "getValue", "()I", null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, ciname, "m_bean", ttype);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, tiname,
gmeth.getName(), "()I");
mv.visitInsn(Opcodes.IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
// generate the setValue method
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "setValue", "(I)V", null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, ciname, "m_bean", ttype);
mv.visitVarInsn(Opcodes.ILOAD, 1);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, tiname,
smeth.getName(), "(I)V");
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
// complete the class generation
cw.visitEnd();
return cw.toByteArray();
}
public static void main(String[] args) throws Exception {
if (args.length == 1) {
ASMCalls inst = new ASMCalls();
inst.test(args[0]);
} else {
System.out.println("Usage: ASMCalls loop-count");
}
}
}
|
The createAccess() code from Listing 3 illustrates
the basic principles of working with ASM.
I start out by creating an org.objectweb.asm.ClassWriter, which accepts a stream of
class events (in the form of method calls) and generates an output binary class
representation. I add a field to the class being constructed by calling the
visitField() method of the writer, which returns a
visitor for that field. The returned field visitor can be used to add
annotations or special attribute information for the field, but in this case I
don't need anything special and can just call the visitEnd() method of the field visitor immediately.
After adding the field, I add the four necessary methods for the class being
constructed. The first method is one that doesn't even appear in the source
code for my template class back in Listing 1, the default
constructor for the class. This constructor, which takes no arguments and only
calls the superclass constructor, is generated automatically by the Java
compiler when you don't specify a constructor for a class. Because I'm building a
class myself, I need to explicitly create the default constructor. The remaining
three methods are the same as shown in the Listing 1 source code.
As with adding a field, the call to the visitMethod() method of the class writer returns a visitor
for the method being added. This method visitor (an instance of the org.objectweb.asm.MethodVisitor interface) can be used to
add annotations or special attributes for the method, but also provides the
interface for generating the actual bytecode instruction sequence that makes up
the body of the method. The Listing 1 code demonstrates how
instructions are appended by calls to the method visitor. When all instructions
have been added, a final pair of calls are used to complete the method
generation. The first one, visitMaxs(), sets the
maximum stack size and local variable count for the method (these values can
alternatively be computed by ASM automatically, configured by passing a true argument in the call to the ClassWriter constructor). The second in this final pair of
calls, visitEnd(), just completes the method-building
process.
Once the field and methods have been added, it's easy to get the binary
version of the completed class. The visitEnd()
call to the class writer indicates that the class writing is complete, and the
toByteArray() call actually returns the binary class
image.
Checking the results
In "Java
programming dynamics, Part 8," I showed timing results comparing both the time
taken to generate the reflection replacement class at run time using Javassist
and BCEL, and the time to execute different numbers of accesses using reflection
versus the replacement class. For this column, I'm going to show the same types
of results but with a few changes. First, I'll include ASM in the
time-to-generate comparisons. I'll also switch to JDK 1.5 for the tests so
that I can use the java.lang.System.nanoTime() method
for more precise timing results.
Figure 1 shows the comparison times for using
reflection method calls versus the generated classes with loop counts ranging from
2K to 512K (tests run on a 1GHz PIIIm notebook system running Mandrake Linux
10.0, using Sun's 1.5.0 JVM). These times are the same across all frameworks.
The performance benefits of using generated code don't look quite as good as
with the 1.4.2 JVM I used in my prior tests, but they are still substantial, with the
generated code running 10 to 14 times faster than reflection.
Figure 1. Reflection vs. generated code speed (time in milliseconds)
The Figure 1 results are interesting, but they aren't really
the main point of this column. More relevant are the results shown in
Table 1, which gives the times taken to construct the
generated class using each framework. Here I've given two separate times for
each framework. The First time value is the time taken for constructing the
first reflection replacement class, which includes the time to load and
initialize the classes in the framework code. The Later times value is an
average for building three more reflection replacement classes (for other
properties).
Table 1. Class construction times
|
Framework
|
First time
|
Later times
| | Javassist | 257 | 5.2 | | BCEL | 473 | 5.5 | | ASM | 62.4 | 1.1 |
The Table 1 results show that ASM does live up to its
billing as faster than the other frameworks, and this advantage applies both to
startup time and to repeated uses.
Conclusions
Matching ASM up against the other classworking frameworks shows that it's
several times faster than the others (at least for this one fairly typical test
case). It's also much more compact, with the run-time JAR weighing in at only
33K (versus 310K for Javassist and a whopping 504K for BCEL). Ease of use is
trickier to determine, but the interface seems significantly cleaner than BCEL's
while offering almost as much flexibility (lacking only some unique features of
BCEL, such as the ability to construct code in segments rather than linearly).
ASM is still not as easy to use as Javassist with its Java-like source code
interface, but if you want to work at the bytecode level, I'd recommend you
look into using ASM.
I'll come back to classworking with ASM in a future column when I discuss
the conversion of a major classworking application originally designed around
BCEL to instead use ASM. For next month, I'm going to look into applying ASM in
another area. I'll review the annotations support added to the Java platform with J2SE 5.0,
and show how ASM can work with J2SE 5.0 annotations to enhance the support in
some very useful ways. Check back then to learn more about this powerful
classworking framework.
Download | Description | Name | Size | Download method |
|---|
| Source code | j-cwt05125code.zip | 912 KB | HTTP |
|---|
Resources - Click the Code icon at the top or bottom of this article (or see Download) to download the source code discussed in this article.
- Be sure to read all the installments in the Classworking toolkit series by Dennis Sosnoski.
- Get the full details on the fast and flexible ASM
Java bytecode manipulation framework.
- Learn more about the Java bytecode design in "Java bytecode: Understanding
bytecode makes you a better programmer" (developerWorks, July 2001) by Peter Haggar.
- For an excellent reference to the JVM architecture and instruction set, see
Inside the Java Virtual Machine by Bill Venners (Artima Software, Inc., 2004). You can view some sample chapters online
to get a look at it before you purchase.
- You can purchase
or view the official Java Virtual Machine Specification online for the definitive word on
all aspects of JVM operation.
- Collect the whole Java
programming dynamics series by author Dennis Sosnoski, taking you on a tour
of the Java class structure, reflection, and classworking.
- If you want to try classworking but don't want to deal directly with the
Java JVM, then Javassist
may be just what you're looking for. Javassist lets you work with source code,
which it compiles into Java bytecode. It's now part of the open source JBoss application server project, where it's the
basis for the addition of new aspect-oriented programming features.
- Get all the details on the popular open source BCEL at the Apache project
page.
- The open source Jikes Project
provides a very fast and highly compliant compiler for the Java programming
language. Use it to generate your bytecode the old fashioned way -- from Java
source code.
- To learn more about Java technology, visit the
developerWorks Java zone. You'll find technical documentation, how-to articles,
education, downloads, product information, and more.
- Visit the New to Java technology site for the latest resources to help you get started with Java programming.
- Get involved in the developerWorks community by participating in
developerWorks blogs.
- Browse for books on these and other technical topics.
About the author  | 
|  | Dennis Sosnoski is the founder and lead consultant of Seattle-area Java consulting company Sosnoski Software Solutions, Inc.,
specialists in J2EE, XML, and Web services support. His professional software development
experience spans over 30 years, with the last several years focused on server-side Java technologies. Dennis is a frequent speaker on XML and Java technologies at
conferences nationwide, and chairs the Seattle Java-XML SIG. Contact Dennis at dms@sosnoski.com. |
Rate this page
|  |