MAJOR: A Tool for Aspect Weaving in the Standard Java Class Library

 

Overview

MAJOR is a tool to weave aspects in the standard Java class library (JDK). It is based on the AJ-UDI which runs on top of FERRARI. MAJOR has a recursive name which stands for Major is AspectJ with Overall Rewriting.


MAJOR is available for download.


MAJOR will be demonstrated at the AOSD’09 conference.


The following sample aspects are included in the MAJOR release:

  ‘exec-tracer’ : simple tracing of all method executions

  ‘call-count’ : counting the number of method executions

  ‘exec-time’ : calculates the cumulated execution time of every method invocation using around()

  ‘wormhole’ : stress test of a simple wormhole between main() and every method invocation. Uses standard cflow(). An efficient implementation is available (see ‘c_wormhole’ in CARAJillo)

  ‘cct’ : creates the calling-context tree using standard thread local shadow stack. An efficient implementation is available without cflow() (see ‘c_cct’ in CARAJillo).

  ‘mem-leak’ : a memory leak candidate detector.

Simple execution tracer ASPECT

Let’s consider a HelloWorld class:


public class HelloWorld {

    public void sayHello() {

        System.out.println(“Hello World”);

    }


    public static void main(String[] args) {

        HelloWorld h = new HelloWorld();

        h.sayHello();

   }

}


and the following ExecTracerAspect:


public aspect ExecTracerAspect {


    pointcut allExecs() : execution(* *(..)) && !within(ExecTracerAspect);


    before() : allExecs() {

       System.out.println("before: " + thisJoinPoint.getSignature());

    }

}


With standard AspectJ weavers (e.g. ajc and aj5 loadtime weaver), you will obtain the trace only of application code.



Compiling the aspect:


cd $MAJOR_HOME/sample/helloworld

ajc ExecTracerAspect.java -outjar aspect.jar -outxml


Running with aj5:


aj5 HelloWorld


before: void HelloWorld.main(String[])

before: void HelloWorld.sayHello()

Hello World


Now, let’s consider the same ExecTracerAspect, with MAJOR.


MAJOR uses AJ-UDI’s support for JDK weaving. Here we can see the transformed aspect using FERRARI’s DIB mechanism. The DIB support is added by an automated tool.


public aspect ExecTracerAspect {


    pointcut allExecs() : execution(* *(..)) && !within(ExecTracerAspect);


    before() : allExecs() {

      boolean oldState = DIB.ensureInstrDisabled();  // generated

      try {

         System.out.println("before: " + thisJoinPoint.getSignature());

      } finally { DIB.setState(oldState); }            // generated

    }


   // Generated wrapper for the compiler generated aspectOf() method.

     public static CallTracerAspect FERRARI$aspectOf() {

        boolean oldState = DIB.ensureInstrDisabled();       

        try {

           return CallTracerAspect.aspectOf();

        } finally { DIB.setState(oldState); }

    }

}


We use the FERRARI’s static tool (FIRST-Ferrari’s Instrumentation and Rewriting Static Tool) to compile and weave the aspect into the JDK:


cd $MAJOR_HOME

first -a exec-tracer -jdk


Then, we can run MAJOR. The output includes not only the execution of the main thread, but also system threads from the startup of the JVM (after bootstrapping) until the shutdown!!!


cd $MAJOR_HOME/samples/helloworld

major -a exec-tracer -jar helloworld.jar


MAJOR started...

before: Class java.lang.ClassLoader.loadClassInternal(String)

before: Class java.lang.ClassLoader.loadClass(String)

before: Class sun.misc.Launcher.AppClassLoader.loadClass(String, boolean)

before: int java.lang.String.lastIndexOf(int)

before: int java.lang.String.lastIndexOf(int, int)

before: SecurityManager java.lang.System.getSecurityManager()

before: Class java.lang.ClassLoader.loadClass(String, boolean)

before: Class java.lang.ClassLoader.findLoadedClass(String)

before: void java.lang.ClassLoader.check()

before: boolean java.lang.ClassLoader.checkName(String)

before: int java.lang.String.length()

before: int java.lang.String.indexOf(int)

before: int java.lang.String.indexOf(int, int)

before: boolean sun.misc.VM.allowArraySyntax()

before: char java.lang.String.charAt(int)

before: Class java.lang.ClassLoader.loadClass(String, boolean)

before: Class java.lang.ClassLoader.findLoadedClass(String)

before: void java.lang.ClassLoader.check()

before: boolean java.lang.ClassLoader.checkName(String)

before: int java.lang.String.length()

before: int java.lang.String.indexOf(int)

before: int java.lang.String.indexOf(int, int)

before: boolean sun.misc.VM.allowArraySyntax()

before: char java.lang.String.charAt(int)

before: Class java.lang.ClassLoader.findBootstrapClass0(String)

before: void java.lang.ClassLoader.check()

before: boolean java.lang.ClassLoader.checkName(String)

before: int java.lang.String.length()

before: int java.lang.String.indexOf(int)

before: int java.lang.String.indexOf(int, int)

before: boolean sun.misc.VM.allowArraySyntax()

...

...

before: int java.util.HashMap.indexFor(int, int)

before: void java.util.HashMap.Entry.recordAccess(HashMap)

before: void HelloWorld.main(String[])

before: void java.lang.ClassLoader.checkPackageAccess(Class, ProtectionDomain)

before: SecurityManager java.lang.System.getSecurityManager()

before: boolean java.util.HashSet.add(Object)

before: Object java.util.HashMap.put(Object, Object)

before: int java.util.HashMap.hash(int)

before: int java.util.HashMap.indexFor(int, int)

before: void java.util.HashMap.Entry.recordAccess(HashMap)

before: void HelloWorld.sayHello()

before: void java.lang.ClassLoader.checkPackageAccess(Class, ProtectionDomain)

before: SecurityManager java.lang.System.getSecurityManager()

before: boolean java.util.HashSet.add(Object)

before: Object java.util.HashMap.put(Object, Object)

before: int java.util.HashMap.hash(int)

before: int java.util.HashMap.indexFor(int, int)

before: void java.util.HashMap.Entry.recordAccess(HashMap)

before: void java.lang.ClassLoader.checkPackageAccess(Class, ProtectionDomain)

before: SecurityManager java.lang.System.getSecurityManager()

before: boolean java.util.HashSet.add(Object)

before: Object java.util.HashMap.put(Object, Object)

before: int java.util.HashMap.hash(int)

before: int java.util.HashMap.indexFor(int, int)

before: void java.util.HashMap.Entry.recordAccess(HashMap)

before: void java.io.PrintStream.println(String)

before: void java.io.PrintStream.print(String)

before: void java.io.PrintStream.write(String)

before: void java.io.PrintStream.ensureOpen()

before: void java.io.Writer.write(String)

before: int java.lang.String.length()

before: void java.io.BufferedWriter.write(String, int, int)

before: void java.io.BufferedWriter.ensureOpen()

before: int java.io.BufferedWriter.min(int, int)

before: void java.lang.String.getChars(int, int, char[], int)

Hello World

before: void java.io.BufferedWriter.flushBuffer()

before: void java.io.BufferedWriter.ensureOpen()

before: void java.io.OutputStreamWriter.flushBuffer()

before: void sun.nio.cs.StreamEncoder.flushBuffer()

before: boolean sun.nio.cs.StreamEncoder.isOpen()

before: void sun.nio.cs.StreamEncoder.implFlushBuffer()

before: int java.nio.Buffer.position()

before: int java.lang.String.indexOf(int)

...

...

before: void sun.misc.VM.addFinalRefCount(int)

before: void java.lang.ref.Finalizer.access$100(Finalizer)

before: void java.lang.ref.Finalizer.runFinalizer()

before: boolean java.lang.ref.Finalizer.hasBeenFinalized()

before: void java.lang.ref.Finalizer.remove()

before: Object java.lang.ref.Reference.get()

before: void java.util.zip.ZipFile.finalize()

before: void java.util.zip.ZipFile.close()

before: int java.util.Vector.size()

before: void java.lang.ref.Reference.clear()

before: ReferenceQueue java.lang.ref.Finalizer.access$000()

before: Reference java.lang.ref.ReferenceQueue.remove()

before: Reference java.lang.ref.ReferenceQueue.remove(long)

before: Reference java.lang.ref.ReferenceQueue.reallyPoll()


You can modify the aspect (in $MAJOR_HOME/aspects/exec-tracer), to e.g. print the thread ID. You will see that the execution captures also system threads. Note that you do not need to weave the full JDK (which may take several minutes) when you only modify the bodies of advices (see the FAQ). To compile and instrument the aspect only, use:


cd $MAJOR_HOME

first -a exec-tracer

call count SAMPLE ASPECT

A sample CallCountAspect counts the number of method calls in the application and within the JDK. It uses a shutdown hook to dump the statistics after the application terminated.


Applying the aspect with standard aspect weaver, we obtain :


cd $MAJOR_HOME/sample/helloworld

ajc CallCountAspect.java -outjar aspect.jar -outxml


aj5 HelloWorld

Hello World


MAJOR's sample CallCountAspect....


Total calls:  3


1  HelloWorld.sayHello()V

1  HelloWorld.<init>()V

1  HelloWorld.main([Ljava.lang.String;)V



Now we weave the CallCountAspect into the JDK:


cd $MAJOR_HOME

first -a call-count -jdk


Using MAJOR with the HelloWorld example, we observe that we can capture all method calls. Most of the method invocations happens within the JDK.


cd $MAJOR_HOME/samples/helloworld

major -a call-count -jar helloworld.jar


MAJOR started...


Hello World


MAJOR's sample CallCountAspect....


Total calls:  2530


296  java.lang.String.charAt(I)C

66  java.lang.String.length()I

55  java.lang.System.getSecurityManager()Ljava/lang/SecurityManager;

49  java.lang.String.equals(Ljava/lang/Object;)Z

46  java.util.jar.Attributes$Name.isAlpha(C)Z

46  sun.misc.ASCIICaseInsensitiveComparator.isUpper(I)Z

46  sun.misc.ASCIICaseInsensitiveComparator.toLower(I)I

46  java.util.jar.Attributes$Name.isValid(C)Z

37  java.lang.CharacterDataLatin1.getProperties(I)I

37  java.lang.CharacterData.of(I)Ljava/lang/CharacterData;

34  java.nio.Buffer.position(I)Ljava/nio/Buffer;

32  java.nio.charset.CoderResult.isUnderflow()Z

29  java.nio.ByteBuffer.arrayOffset()I

29  java.lang.Character.toUpperCaseEx(I)I

29  java.nio.Buffer.position()I

29  java.lang.CharacterDataLatin1.toUpperCaseEx(I)I

27  java.lang.String.indexOf(II)I

27  java.nio.CharBuffer.arrayOffset()I

25  java.lang.String.indexOf(I)I

24  java.lang.String.startsWith(Ljava/lang/String;I)Z

22  sun.misc.VM.allowArraySyntax()Z

22  java.lang.ClassLoader.check()V

22  java.lang.ClassLoader.checkName(Ljava/lang/String;)Z

21  java.util.HashMap.indexFor(II)I

...

1  java.io.FilePermissionCollection.add(Ljava/security/Permission;)V

1  java.lang.RuntimePermission.<init>(Ljava/lang/String;)V

1  HelloWorld.main([Ljava.lang.String;)V

1  java.util.jar.Attributes.getValue(Ljava/lang/String;)Ljava/lang/String;

...

1  java.net.URL.toString()Ljava/lang/String;

1  java.lang.ClassLoader.addClass(Ljava/lang/Class;)V

1  HelloWorld.sayHello()V

1  java.io.PrintStream.write(Ljava/lang/String;)V

1  java.util.zip.ZipFile.<init>(Ljava/io/File;I)V

1  java.util.zip.Inflater.<init>(Z)V

...

...

MEmory LEAK SAMPLE ASPECT

This example is described in our PPPJ’08 paper.


Consider the following code, which has a memory leak.


public class LeakExample {


    static Vector<String> myVector = new Vector<String>();


    public void slowlyLeakingVector(int iter, int count) {

      for (int i=0; i<iter; i++) {

         for (int n=0; n<count; n++) {

            myVector.add(Integer.toString(n+1)); // no explicit “new”

         }

         for (int n=count-1; n>0; n--) {

            // Oops, it should be n>=0 (memory leak)

            myVector.removeElementAt(n);

         }

      }

   }

   ...

}


We have developed a LeakDetectorAspect that captures each object and array allocation, keeps a weak reference to the allocated object together with a Throwable instance containing the stack trace. We analyze the references non-garbage collected as potential memory leak candidates.


When calling slowLeakingVector(1000,10), we expect to have 1000 leaking objects (1 leaking object per iteration). However, when using standard aspect weaver (without MAJOR), only 1 non-reclaimed object is detected.



Compiling the aspect:


cd $MAJOR_HOME/samples/mem-leak

ajc -1.5 LeakDetectorAspect.java -outjar aspect.jar -outxml


Running with aj5:


aj5 LeakExample


Leak Example start, 1000 iterations, 10 counts

Done

Total number of allocated objects:      2

Total number of non-reclaimed objects:  1


Details of non-reclaimed objects (potential memory leaks):


Number of occurrences: 1

Location and stack trace: constructor-call  java.util.Vector() LeakExample.java:6

=>LeakExample.<clinit>(LeakExample.java:6)


The problem is that the application code does not have an explicit allocation instruction (“new”). Therefore, the aspect cannot capture the leaking objects creation (their allocation is ‘hidden’ inside Integer.toString() which actually allocate the leaking objects, i.e. allocation is made within the JDK).


Thanks to MAJOR, we can capture all allocations:


cd $MAJOR_HOME

first -a mem-leak -jdk


We now apply the LeakDetectorAspect to the application with a woven JDK:


cd $MAJOR_HOME/samples/mem-leak

major -a mem-leak -jar leakexample.jar


MAJOR started...

Leak Example start, 1000 iterations, 10 counts

Done

Total number of allocated objects:      20173

Total number of non-reclaimed objects:  2071


Details of non-reclaimed objects (potential memory leaks):


Number of occurrences: 1

Location and stack trace: constructor-call  java.util.ArrayList()  JarVerifier.java:90

=>java.util.jar.JarFile.getManifest(JarFile.java:163)

=>java.util.jar.JarFile.getManifestFromReference(JarFile.java)

  =>java.util.jar.JarVerifier.<init>(JarVerifier.java)


Number of occurrences: 1

Location and stack trace:constructor-call  java.util.Vector()  LeakExample.java:6

=>LeakExample.<clinit>(LeakExample.java:6)



Number of occurrences: 1000

Location and stack trace: constructor-call  java.lang.String(int, int, char[])  Integer.java:331

=>LeakExample.main(LeakExample.java:28)

=>LeakExample.slowlyLeakingVector(LeakExample.java:11)

  =>java.lang.Integer.toString(Integer.java)



Number of occurrences: 1000

Location and stack trace: constructor-call  char[](int)  Integer.java:329

=>LeakExample.main(LeakExample.java:28)

=>LeakExample.slowlyLeakingVector(LeakExample.java:11)

  =>java.lang.Integer.toString(Integer.java)



Number of occurrences: 4

Location and stack trace: constructor-call  char[](int)  String.java:2737

=>java.lang.Thread.<init>(Thread.java)

=>java.lang.Thread.init(Thread.java)

  =>java.lang.String.toCharArray(String.java)


...

...


Number of occurrences: 1

Location and stack trace: constructor-call  java.security.Principal[](int)  ProtectionDomain.java:146

=>java.lang.ClassLoader.loadClassInternal(ClassLoader.java:338)

=>java.lang.ClassLoader.loadClass(ClassLoader.java:270)

  =>sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java)

   =>java.lang.ClassLoader.loadClass(ClassLoader.java)

    =>java.net.URLClassLoader.findClass(URLClassLoader.java)

     =>java.security.AccessController.doPrivileged(Native Method)

      =>java.net.URLClassLoader$1.run(URLClassLoader.java:209)

       =>java.net.URLClassLoader$1.run(URLClassLoader.java)

        =>java.net.URLClassLoader.access$000(URLClassLoader.java:73)

         =>java.net.URLClassLoader.defineClass(URLClassLoader.java)

          =>java.security.SecureClassLoader.defineClass(SecureClassLoader.java)

          


We can see that we obtain the 1000 leaking String objects allocated by Integer.toString(). In addition, we can observe that there are additional 1000 objects are created. This is consistent with the fact that (in several VMs) for each String object there is always an array of characters (char[]) that is allocated.