In general, the instrumentations I’m doing can be grouped into two categories: Those that only require local modification of a class file, and those that require changes to all the other class files that use it as well.
Instrumentations with Local Effects
The first case is a lot easier to handle. Instrumenting Thread.*
(except for sleep
and yield
) and instrumenting synchronized blocks belongs here. As mentioned already, in the former case I just rename the old method and substitute a new method that logs and forwards. Because this method has the same name, the classes that use the method will never know anything changed. In the latter case, I just insert log calls before and after the MONITORENTER
and before the MONITOREXIT
opcodes. This only affects the code locally.
Instrumentations with Global Effects
Unfortunately, there are other events that need to be monitored. Object.*
, Thread.sleep
and Thread.yield
, for example, are native methods and cannot be renamed. For these methods, I introduce an additonal wrapper method that does the logging and forwarding. Then I need to scan through all other files and make calls to the original method point to the wrapper method.
Synchronized methods automatically claim a lock, there’s no opcode that I can look for, so the “trying to enter” event must actually be handled at the call site, as opposed to inside the method. That means I need to go through all files and insert “trying to enter” log calls before calls to synchronized methods. That, of course, also means that I have to know what methods are synchronized in the first place.
In synchronized native methods, I can’t insert the “entered” log call, so I need to follow a wrapper approach, just like with the Object.*
methods.
Results
The upshot of instrumentation with global effects is that you cannot perform all instrumentations on just a few classes in isolation. If you instrument Object.*
, Thread.sleep
, Thread.yield
, or synchronized methods in one class, you have to make corresponding changes in all classes that use it.
Since I’ve run into trouble instrumenting the entire Java runtime, we’ve been considering not instrumenting all portions of it and maybe leaving large portions of java.lang.*
uninstrumented. Of course, the Thread
and Object
classes have to be instrumented, but others could be left out.
With global effects, though, an arbitrarily partial instrumentation might not be possible. It seems like we need to instrument at least something akin to the smallest transitive closure of classes. Fortunately, the way I have global-effect instrumentations set up right now, a program should work even if it is not instrumented correctly. An uninstrumented class would just call the original method, not the wrapper. The program couldn’t be monitored correctly, but it would at least work.
That doesn’t seem like a very helpful thing, since we really need complete information about all the events, but depending on the semantic model we use, it might save the day: If we assume that certain portions of the runtime are thread-safe, then we can safely lose some or all synchronization information about that portion.