Published on dev2dev (http://dev2dev.bea.com/)
 http://dev2dev.bea.com/pub/a/2005/08/jvm_aop_1.html
 See this if you're having trouble printing code examples

JRockit JVM Support For AOP, Part 1

by Jonas Bonér and Alexandre Vasseur, Joakim Dahlstedt
08/01/2005

Abstract

Aspect-Oriented Programming (AOP) is gaining momentum in the software community and the enterprise space at large. Introduced by Xerox back in the 1990s, AOP has been getting more and more mature through several initiatives and innovations in the research community, the open-source community, and the enterprise arena. In the Java arena, open source has achieved great traction in the last two years, leading to the recent merger of AspectWerkz and AspectJ, now housed at the Eclipse Foundation with the code name AspectJ 5. AspectJ is sponsored by BEA Systems and IBM and can be considered as the de-facto standard for AOP in Java.

As the popularity of AOP is growing and the research community is moving things forward, vocabulary, concepts, and implementations have gained in consistency, allowing for better tool support and developer experience, such as with the AspectJ Eclipse plug-in AspectJ Development Tools (AJDT).

AOP has also gone through several implementation techniques, ranging from source-code manipulation to bytecode instrumentation—a technique widely adopted in Java, especially with the advent of the Java 5 JVMTI. This technique is now used by several AOP-applied, enterprise-grade products in the area of application management and monitoring and has gained even greater popularity more recently with Plain Old Java Object (POJO)-based middleware and transparent clustering.

As a consequence, bytecode instrumentation is increasingly likely to be the thing you'll finally have to be made aware of, despite all the promises. Questions you'll have to answer include: Up to what point can bytecode instrumentation techniques scale and achieve manageability, transparency, and efficiency? Is there a risk that AOP implementations that rely on bytecode instrumentation will reach an endpoint that limits further innovation toward more efficiency, ease of use, and dynamism? Would JVM-level support for AOP solve these issues and to what extent? This article series aims to provide concrete answers to these questions by unveiling BEA JRockit JVM AOP support, as well as stimulating debate in this space.

This first article introduces AOP concepts and describes briefly why many implementations, for example AspectJ, are based on bytecode manipulation. It explains several limitations that are tied to bytecode instrumentation techniques and why they will affect both scalability and usability in the long run. The last section then introduces JRockit JVM support for AOP, which aims to address these limitations to provide an efficient backend for AOP and other interception mechanisms.

The second part of this article series will then unveil how far this support goes through concrete API details and samples.

What is Aspect-Oriented Programming?

Object-oriented analysis and design has given us tools to reduce software complexity by introducing concepts like inheritance, abstraction, and polymorphism. However, developers face daily problems in software design that can't be solved easily using object-oriented software development. One of these problems is how to handle cross-cutting concerns in the application.

Cross-cutting concerns

A concern is a particular concept or area of interest. For example, in an ordering system the core concerns could be order processing and manufacturing, while the system concerns could be transaction handling and security management.

A cross-cutting concern is a concern that affects several classes or modules, a concern that is not well localized and modularized.

Symptoms of a cross-cutting concern are:

These symptoms affect software in various ways; for example, they make it harder to maintain and reuse software as well as harder to write and understand.

Separation of concerns

Aspect-Oriented Programming tries to solve these problems by introducing the concept of separation of concerns, in which concerns can be implemented in a modular and well-localized way. AOP solves this by adding an extra dimension to the design space, and introduces constructs that allow us to define the cross-cutting concerns—to lift them out into a new dimension and package them in a modular way.

New constructs introduced by AOP

AOP introduces some new constructs. The join point construct mirrors well-defined points in the program flow, for example, where a method is invoked or where an exception is caught. The pointcut construct lets us pick out join points that match certain criteria. The advice construct lets us add code that should be executed at matching join points. The introduction construct lets us add additional code to existing classes, for example, adding methods, fields, or interfaces to an existing class. Finally, AOP introduces the aspect construct, which is AOP's unit of modularity. An aspect is defined in terms of join points, pointcuts, advice, and introductions (also referred as inter-types declarations).

Samples of AOP in AspectJ

The following are simple code samples of AspectJ 5 that partially illustrate how the concepts defined above are mirrored in code. For further reference on specific AOP language details, refer to the AspectJ documentation.

// using a dedicated syntax
// that compliments the Java language
public aspect Foo {

  pointcut someListOperation() : call(* List+.add(..));

  pointcut userScope() : within(com.biz..*);

  before() : someListOperation() && userScope() {
    System.out.println("called: "
        + thisJoinPoint.getSignature()
    );
  }
}

The above code uses a dedicated syntax. You can write the equivalent code using Java annotations:

// the Java 5 annotations approach
@Aspect
public class Foo {

  @Pointcut("call(* java.util.List+.add(..))")
  public void someListOperation() {}

  @Pointcut("within(com.biz..*)")
  public void userScope() {}

  @Before("someListOperation() && userScope()")
  public void before(JoinPoint thisJoinPoint) {
    System.out.println("called: "
        + thisJoinPoint.getSignature()
    );
  }
}

The above code defines an aspect Foo with two pointcuts named someListOperation() and userScope(). These pointcuts will pick a set of join points in the application. They are composed together as a boolean expression someListOperation() && userScope() so that the before advice is executed before every call to any method named add on any instance of a type that extends List, providing that this call is done from within some code that stands in the com.biz package (and subpackages). The before advice will then print the signature of the method about to be called at any of those join points. The second code sample defines the very same aspect in an alternative syntax that relies on Java 5 annotations.

What is Weaving?

As described in the previous section and illustrated in the code samples, aspects can cross-cut the entire application. Weaving is the process of taking the aspects and the regular object-oriented application and "weaving" them into one single unit, one single application.

Weaving can happen at different periods in time:

This process can also be done in many different ways:

Source-code weaving is limited in the sense that all source code must be available and presented to the weaver so that aspects can be applied. This makes it impossible, for example, to implement generic monitoring services. Compile-time weaving suffers from the same problem: All bytecode that will be deployed needs to be prepared before deployment in a post-compile-time phase.

This article series covers bytecode weaving vs. JVM weaving and will be discussed in the following sections.

As a side note, Dynamic proxies, a limited form of weaving, has been available in the JVM for some time. This API has been part of the JDK since 1.3, and it allows you to create a dynamic, virtual proxy of an interface (and/or a list of interfaces), which gives you the possibility of intercepting each invocation to the proxy and redirecting it anywhere you want. This is not really weaving by definition, but it resembles it in that it provides a simple way of doing method interception. It is used by various frameworks to do simple AOP, for example, the Spring Framework.

Problems with Bytecode Instrumentation-based Weaving

It is worth emphasizing that the problems described below are tied to bytecode instrumentation and, as a consequence, current AOP implementations such as AspectJ are suffering from it. These problems affect all bytecode instrumentation-based products in general, such as application monitoring solutions, profiling tools, or other AOP-applied solutions.

Instrumentation is inefficient

The actual instrumentation part of the weaving is usually very CPU-intensive, and sometimes also consumes a significant amount of memory. This can affect startup time. For example, to intercept all calls to the toString() method or all access to a certain field, you need to parse almost every single bytecode instruction in all classes, one by one. This also means that a lot of intermediate representation structures will be created by the bytecode instrumentation framework to expose the bytecode instructions in a usable way. This could potentially mean that the weaver needs to parse all bytecode instructions in all classes in the whole application (including third-party libraries and so on). In bad cases, this could amount to more than 10,000 classes.

If more than one weaver is used, the overhead will be multiplied.

Double bookkeeping: Building a class database for the weaver is expensive

To know whether or not a class, method, or field should be woven, the weaver needs to do matching on metadata for this class or member. Most AOP frameworks and AOP-applied products have some sort of high-level expression language (pointcut expressions) to define where (at which join points) a code block (advice) should be woven in. These expression languages can, for example, let you pick out all methods that have a return type that implements an interface of type T. This information is not available in the bytecode instructions representing the call to a specific method M. The only way of knowing whether or not this specific method M should be woven is to look it up in some sort of class database, query its return type, and check if its return type implements the given interface T.

You may be thinking: Why not just use the java.lang.reflect.* API? The problem with using reflection here is that there's no way to query a Java type reflectively without triggering the class loading of this particular class, which will trigger the weaving of this class before we know enough about it to do the weaving (in load-time weaving infrastructures). Simply put: We end up with the classic chicken-and-egg problem.

The weaver therefore needs a class database (usually built up in memory from the raw bytecode read from the disk) to do the required queries on if needed for the actual join points. This problem can sometimes be avoided by limiting the expression language expressiveness, but that usually limits the usability of the product.

This in-memory class database is also redundant once the weaving is done. The JVM already has all this information in its own database, well optimized, (for example, it serves the java.lang.reflect API). So we end up doing double bookkeeping of the whole class structure (object model), which consumes significant and unnecessary memory, as well as adds a startup cost in creating this class database and maintaining it when a change occurs.

If more than one weaver is used, the overhead will be multiplied.

HotSwap: Changing bytecode at runtime adds more complexity

Java 5 brought the HotSwap API as part of the JVMTI specification. Before Java 5, this API was only available when running in debug mode, and only for native C/C++ JVM extensions. This API allows the changing of bytecodes—that is, redefining a class—at runtime. It is used by some AOP frameworks and AOP-applied products to emulate runtime weaving capabilities.

Despite being very powerful, this API limits usability and scalability in these ways:

Furthermore, none of the current implementations of the HotSwap API supports schema change, which the specification states as being optional. This means that it is not possible to change the schema of a class at runtime, for example, adding methods/fields/interfaces that might be needed for the underlying instrumentation model. This makes it impossible to implement certain types of runtime weaving and therefore requires the user to "prepare" the classes in advance.

Multiple agents is a problem

When multiple products are using bytecode instrumentation, unexpected problems may happen. Problems related to precedence, notification of changes, undoing of changes, and so on. While this may not be a big problem today, it will be a significant problem in the future. A weaver can be seen as an agent (as referred to in the JVMTI specification) that performs instrumentation at loadtime or runtime. With multiple agents, there is a high risk that the agents will get in each other's way, changing the bytecode in a way that was not expected by the next agent, which makes the assumption that it is the sole configured agent.

Here is an example of a problem that can occur when two agents are unaware of each other. If one is using two agents, an AOP weaver and some application performance product, that are both doing bytecode instrumentation at loadtime, it is likely that, depending on the configuration, woven code may or may not be part of the performance measurement, as illustrated below:

// say this is the original user code
void businessMethod() {
  userCode.do();
}

//---- Case 1
// say the AOP weaver was applied BEFORE the
// performance management weaver
// the woven code will behave like:
void businessMethod() {
  try {
    performanceEnter();
    aopBeforeExecuting();//hypothetical advice
    userCode.do()
  } finally {
    performanceExit();
  }
}
// ie the AOP code affect the measure


//---- Case 2
// say the AOP weaver was applied AFTER the
// performance management weaver
// the woven code will behave like:
void businessMethod() {
  aopBeforeExecuting();//hypothetical advice
  try {
    performanceEnter();
    userCode.do()
  } finally {
    performanceExit();
  }
}
// ie the AOP code will NOT affect the measure

We have a problem with precedence between the agents; there is no fine-grained configuration to control the ordering at a join point (or pointcut) level.

Some other situations may lead to more unpredictable results. For example, when a field access is intercepted, it usually means that the field get bytecode instructions are moved to a newly added method and replaced by a call to this new method. The next weaver will therefore see a field access from another place in the code (from this newly added method) that then may not be matched by its own matching mechanism and configuration.

To summarize, the main problems are:

Intercepting reflective calls is impossible

Current weaving approaches can only instrument execution flows that can be (at least partially) statically determined. Consider the following code sample that invokes the method void doA() on the given instance foo.

public void invokeA(Object foo) throws Throwable {
  Method mA = foo.getClass().getDeclaredMethod("doA", new Class[0]);
  mA.invoke(foo, new Object[0]);
}

This kind of reflective access is often used in modern libraries to create instances, to invoke methods, or to access fields.

From a bytecode perspective, the call to the method void doA() is not seen. The weaver will see only calls to the java.lang.reflect API. There is no simple and performant way of weaving calls that are made reflectively. This is an important limitation in how weaving can be performed and how AOP is implemented today. Best practices recommend that the developer use execution side pointcuts instead. Obviously, from a JVM perspective, there will be a method dispatch to the doA() method, even it does not appear in the source code or bytecode. JVM weaving has proven to be the only weaving mechanism that addresses this issue in an efficient way.

Other problems

Bytecode instrumentation, especially when done on the fly (at loadtime or runtime), is looked upon with skepticism by some people. There is an emotional angle to changing code on the fly that should not be underestimated, especially when it is paired with a mind-bending revolutionary new technology such as AOP or transparent injection of services. Clashes that may happen when multiple agents are involved will increase this skepticism.

Another potential problem is the 64Kb boundary for class files as stated in the Java specification. Method bodies are limited to a 64Kb total bytecode instruction size. This may pose a problem when weaving already large class files, like, for example, the resulting class file when compiling a JSP file to a servlet. When instrumenting this class, it may break the 64Kb limit, and then cause a runtime error.

Proposed solution

JVM weaving is the natural answer to most of the issues discussed above. To understand why, we will look at a couple of examples that show the JVM is already doing most of the work necessary to do weaving: When a class gets loaded, the JVM does read the bytecode to build up the data needed to serve the java.lang.reflect.* API purpose. Another example is method dispatching. Modern JVMs compile the bytecode of methods or code blocks to more advanced and efficient constructs and execution flows (doing code inlining where applicable). Due to the HotSwap API requirements, the JRockit JVM (and probably other JVMs too) also does bookkeeping on which method calls which other method, so that a method body can still be hotswapped in all expected places—inlined or not—if its defining class is redefined at runtime.

As a consequence, instead of changing the bytecode to weave in an advice invocation— say before a specific method call—the JVM could actually have knowledge about it and simply do a dispatch to the advice at any matching join point prior to dispatching to the actual method.

As bytecode would be untouched, immediate advantages can be expected, such as:

A detailed description of the proposed JRockit JVM support for AOP will be presented in the second article of this series.

The following code sample serves as the concluding illustration. It dispatches to the static method advice() just before the sayHello() method gets called :

public class Hello {

  // -- the sample method to intercept
  public void sayHello() {
    System.out.println("Hello World");
  }

  // -- using the JRockit JVM support for AOP
  static void weave() throws Throwable {
    // match on method name
    StringFilter methodName = new StringFilter(
        "sayHello",
        StringFilter.Type.EXACT
    );

    // match on callee type
    ClassFilter klass = new ClassFilter(
        Hello.class,
        false,
        null
    );

    // advice is a regular method dispatch
    Method advice = Aspect.class.getDeclaredMethod(
        "advice",
        new Class[0]
    );

    // get a JRockit weaver and subscribe the
    // advice to the join point picked out by the filter
    Weaver w = WeaverFactory.createWeaver();

    w.addSubscription(new MethodSubscription(
        new MethodFilter(
            0,
            null,
            klass,
            methodName,
            null,
            null
        ),
        MethodSubscription.InsertionType.BEFORE,
        advice
    ));
  }

  // -- sample code

  static void test() {
    new Hello().sayHello();
  }

  public static void main(String a[])
  throws Throwable {
    weave();
    test();
  }

  // -- the sample aspect

  public static class Aspect {

    public static void advice() {
        System.out.println("About to say:");
    }
  }
}

Conclusion

Bytecode instrumentation has gained popularity in the Java community to implement advanced technologies such as AOP or transparent services addition in the middleware space. However, it suffers from several key limitations, and its widespread use will cause further issues affecting both scalability and usability.

Since bytecode instrumentation has more or less become the standard way of implementing weaving in AOP, it will suffer, if it hasn't already, from the limitations and problems outlined in this article.

We believe that JVM support for AOP is the natural solution to those problems. We are proposing a subscription-based API that we have implemented in the JRockit JVM, which is closely integrated with the JVM method dispatch internals. The next article in this series will illustrate in more detail the API, and explain how each problem is solved.

Additional Reading

Jonas Bonér is a senior software engineer at the JRockit team at BEA Systems and is currently working on dynamic AOP, VM weaving and AOP evangelism. He is the founder of the AspectWerkz AOP framework and committer to the AspectJ 5 project.

Joakim Dahlstedt is the CTO of the Java Runtime Products Group at BEA Systems, Inc., where he is responsible for the future development directions for the JVM.

Alexandre Vasseur works as a software engineer at BEA Systems within the Java Runtime Product Group, focusing on Aspect Oriented Technology. He is the co-founder of the AspectWerkz AOP framework and committer on Eclipse AspectJ 5.


Return to dev2dev.