Skip to main content

Classworking toolkit: ASM classworking

Does the ASM classworking library claim of being both small and fast hold up? Put it to the test using ASM 2.0

Dennis Sosnoski, President, Sosnoski Software Solutions, Inc.
Photo of Dennis Sosnoski
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.

Summary:  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.

View more content in this series

Date:  12 May 2005
Level:  Intermediate
Activity:  2138 views
Comments:  

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.

Ask the expert: Dennis Sosnoski on JVM and bytecode issues

For comments or questions about the material covered in this article series, as well as anything else that pertains to Java bytecode, the Java binary class format, or general JVM issues, visit the JVM and Bytecode discussion forum, moderated by Dennis Sosnoski.

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)
Reflection vs. generated code speed

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
FrameworkFirst timeLater times
Javassist2575.2
BCEL4735.5
ASM62.41.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

DescriptionNameSizeDownload method
Source codej-cwt05125code.zip912 KB HTTP

Information about download methods


Resources

  • 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

Photo of Dennis Sosnoski

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.

Comments



Trademarks

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology, Open source
ArticleID=83014
ArticleTitle=Classworking toolkit: ASM classworking
publish-date=05122005
author1-email=dms@sosnoski.com
author1-email-cc=