This subject has been discussed in a few other blogs (search on the package name); I'm sharing my experience because some of the pitfalls haven't been covered too well in other postings, and the more hits you can get on a particular stack trace, the better.
If you've used JDK tools like Java VisualVM or JConsole, you've seen the attach API at work. If you're going to attach to a running process, you need to get a handle for it (a VirtualMachineDescriptor, specifically), which implies a method for listing processes (namely, VirtualMachine.list()). These are the process descriptors you see in the above tools' connect dialogs at launch time. The best place to start is to look at the API documentation for com.sun.tools.attach. This information can be found in the tools documentation of your JDK documentation download, or online. From there, take a look at the documentation for the com.sun.tools.attach.VirtualMachine class -- it provides an outline for getting started. I'll start with this, pointing out some of the issues you can run into along the way.
The basic process is outlined below:
- Create a public static void agentmain() method in your profiling agent (if you have already created a launch-time profiling agent, this can contain the same logic as your premain() method).
- Add an Agent-Class: attribute to your profiler's manifest file, specifying the fully-qualified name of your profiler class
- In your launch application, perform the following steps:
- Obtain a descriptor for the target JVM.
- Obtain a VirtualMachine instance with the static VirtualMachine.attach() method.
- Load your profiling agent into the target JVM with the VirtualMachine.loadAgent() method.
- Detach from the target JVM
- At this point, your profiler's agentmain() method will be called and you are in business.
Note that on Windows, a String version of the target JVM's Windows process ID can be passed as the id for the VirtualMachine.attach() method.
A number of issues can arise during this process. First, note that you need the full JDK, not just the JRE, to do this. The reason is that you need the JDK's tools.jar file.
One of the first issues you might see is the following:
java.lang.UnsatisfiedLinkError: no attach in java.library.path
AttachNotSupportedException: no providers installed
com.sun.tools.attach.AttachNotSupportedException: no providers installed
My diagnosis of this problem is that I was executing java on the command line, which as a result of my JDK and JRE installation choices was pointing to the java.exe in the %JAVA_HOME%/jre/bin directory. While this is technically the same executable, you will find that when using the instrumentation packages, the java executable expects to find tools.jar using a relative path, and that path is relative to the java.exe in the %JAVA_HOME%/bin directory. So ensure at launch time that you are running the java executable in the JDK bin, not the JRE bin, directory.
Another more interesting problem you can run into when adding your agent jar to the target JVM is that the target JVM doesn't necessarily know about the dependencies of your agent. For example, all agents will have a dependency on tools.jar. The one I have been building has an additional dependency on the Javassist javassist.jar file. If your target application does not have these files on its classpath, it will fail to start your agent. For the record, in the console window (if you have one) for your target JVM (not the console from which you are trying to attach), you'll see something like
Exception in thread "Attach Listener" java.lang.NoClassDefFoundError: com/sun/tools/attach/AgentInitializationException
But you can't very well require that all potential target applications be launched with your dependencies on their classpath. So what to do?
The answer lies in the Boot-Class-Path: attribute in the profiling agent manifest file (see the package summary documentation for java.lang.instrument). Entries in this attribute are space-separated jar files. So to get past this issue, I added the paths to tools.jar and javassist.jar to this attribute in my profiler's manifest file.
At this point, you should be able to launch your agent. When you launch your profiler agent at target JVM launch time, your class transformer will be called as each class is loaded. When you launch your profiler by attaching to an already-running JVM, you will need to transform classes which have already been loaded. In my profiler, I do this by calling the getAllLoadedClasses() method of the java.lang.instrument.Instrumentation instance, then calling retransformClasses() on them. As you retrieve this set of classes, you might notice that you can check to see if the class is modifiable (using another method in the Instrumentation instance).
Note that according to the java.lang.instrument package summary documentation, you need the manifest attribute Can-Retransform-Classes: to be able to retransform a class. This attribute is not related to instrumenting at launch time versus after launch time (in either case, you could instrument the target application by transforming its classes only one time), but rather is related to the issue of transforming the same files multiple times (for example, to remove your profiling code). I'll discuss retransforming classes a little later.
Another important point to keep in mind is the call to add your transformer to your Instrumentation instance: this should set the canRetransform attribute to true (use the 2-argument signature addTransformer(ClassFileTransformer, boolean), since the default value for the attribute is false). Although the class files had not yet actually been transformed at attach time (when I launch my profiler by attaching to an already-running JVM), I noticed in my own case that if the canRetransform attribute is false, none of my methods get instrumented. I believe the reason for this behavior is simply that I am calling retransformClasses() when I attach to an already-running JVM (I see no other available method to use), and that method will not invoke the transformer's transform() method if the canRetransform attribute is false.
I specifically ignore the classes of my own profiler when I perform this operation -- in my case I get a stack overflow error if I try to method-entry and method-exit instrument the very methods which process these events! You may have similar issues if you are building a similar type of profiler. I also sometimes get odd and unrepeatable (including core-dumping) behavior if I attempt to instrument core Java classes, so I am more inclined to exempt those from profiling (at least of the method-entry/exit kind).
An interesting question to consider is what happens when you terminate your profiling agent. Mine is included in a Swing application. Note that since your agent is launched in the same JVM as the target, you might not want to call System.exit() anywhere -- your profiler users (including yourself) may not want to kill their target application just to close your profiler. But thinking about this brings up another interesting question -- when you close your profiler, what happens to the instrumented classes in your target application? The short answer is that they continue to be instrumented, possibly at significant cost to performance. You don't want to force your profiler users to bounce their application to rid themselves of your bytecodes. So the question is: how to get the target application back to its pre-instrumented state?
Some clues to this question can be found (again!) in the package documentation, specifically the retransformClasses() method of java.lang.instrument.Instrumentation. The description of this method states that this method starts with the intial class bytes of the class, before it was transformed (some differences may exist with the original class bytes, but they are considered cosmetic in the classfile-structure sense). Further reading of this description suggests that we should be able to revert to the original class bytes by (1) removing our current bytecode-manipulating transformer from our Instrumentation instance, and (2) calling retransformClasses() on all classes. Doing this should result in reversion to uninstrumented code, something that we should politely do before closing our profiler. Besides, not doing so results in strange behavior if you launch another profiler against the same running JVM, since you will be re-instrumenting classes you had already instrumented with your previous profiler instance.
Looking at what we need to do to make this work, probably the safest thing to do is to always outfit the Instrumentation class with no ClassFileTransformer and call retransformClasses() first, before then attempting to instrument the target JVM's classes. This behavior will ensure that you are always starting with the original class bytes (at target JVM launch time) before you transform. Then, at profiler-close time, perform the same instrumentation-clearing operation. In other words, the code would look something like:
// initialization code:
MyTransformer transformer = new MyTransformer();
instrumentationInstance.addTransformer(transformer);
...
// to instrument, first revert all added bytecode:
instrumentationInstance.removeTransformer(transformer);
// call retransformClasses() on all modifiable classes...
instrumentationInstance.addTransformer(transformer);
// call retransformClasses() on all modifiable classes...
...
// before exiting application:
instrumentationInstance.removeTransformer(transformer);
// call retransformClasses() on all modifiable classes...
Next, I will verify that this approach works by implementing the above code and running a debug version of my profiler to see if my instrumentation code has been removed.
My first concession to efficiency was to skip the initial revert-to-uninstrumented step, and only revert when I know that the classes have actually been instrumented. The reason is that this is a fairly expensive operation and it isn't necessary to perform when the target application is being instrumented for the first time.
The second issue I had was specific to Javassist. Once certain operations have been performed on a Javassist class model, that class is "frozen" and cannot be retransformed without first "defrosting" it (see Getting Started with Javassist for details). I noticed that, in spite of the fact that I was creating a new Javassist class instance from classfile bytecodes, I get a "class is frozen" RuntimeException when I attempt to transform classes an additional time, so I got around the issue with code like the following:
CtClass cc = cp.get(javassistClassName);
// for repeated instrument/uninstrument cycles, // make sure the class is unfrozen before we // attempt to do anything to it: if (cc.isFrozen()) { cc.defrost(); }
Looking at the above code reminds me of a minor point you should know -- the fully-qualified class names that the Java runtime passes to your class file transformer's transform() method are slash delimited, while Javassist expects dot (".") delimiters. So to match names, you need to something like the following:
String javassistClassName = className.replace('/', '.');
where className is the class name as passed in to transform().
The above procedure should allow you to stop and start profiling (by un-instrumenting and instrumenting classes) and also to clean up before you close your profiler application, so you don't leave expensive profiling code in your target application at profiler disconnect time. I tested this procedure in my profiler with a pair of stop/start buttons and using Javassist (and the defrost() method shown above) and verified that the target application does indeed revert to pre-instrumented bytecodes.
I keep saying I'm going to do something really interesting with this profiling data (and I will!), but in the process of investigating this API I decided it merited its own entry. Soon, and maybe in my next post, I'll discuss generating events from this data and running them through an event processor.

1 comments:
Thanks for your post, it is very interesting.
but I have some problems to implement this technique.
Can you help me by sending me an exemple of source code at pierre.caserta@loria.fr
Best Regards
Pierre Caserta
Post a Comment