jmethodIDs identify methods in many low-level C++ JVM API methods (JVMTI). These ids are used in debugging related methods like SetBreakpoint(jvmtiEnv*,jmethodID,jlocation)
and, of course, in the two main profiling APIs in the OpenJDK, GetStackTrace, and AsyncGetCallTrace (ASGCT):
JVMTI has multiple helper methods to get the methods name, signature, declaring class, modifiers, and more for a given jmethodID. Using these IDs is, therefore, an essential part of developing profilers but also a source of sorrow:
In this blog post, I will tell you about the problems of jmethodID that keep profiler writers awake at night and how I intend to remedy the situation for profiler writers in JEP 435.
Background
But first: What are jmethodIDs, and how are they implemented?
[A jmethodID] identifies a Java programming language method, initializer, or constructor.
JVMTI SPECIFICATIONjmethodID
s returned by JVMTI functions and events may be safely stored. However, if the class is unloaded, they become invalid and must not be used.
In OpenJDK, they are defined as pointers to an anonymous struct (source). Every Java method is backed by an object of the Method
class in the JDK. jmethodIDs are actually just pointing to a pointer that points to the related method object (source):
This indirection creates versatility: The jmethodID stays the same when methods are redefined (see Instrumenting Java Code to Find and Handle Unused Classes for an example of a Java agent which redefines classes).
This is not true for jclass, the jmethodID pendant for classes that points directly to a class object:
The jclass becomes invalid if the class is redefined.
jmethodIDs are allocated on demand because they can stay with the JVM till the defining class is unloaded. The indirections for all ids are stored in the jmethodID cache of the related class (source). This cache has a lock to guard its parallel access from different threads, and the cache is dynamically sized (similar to the ArrayList implementation) to conserve memory.
OpenJ9 also uses an indirection (source), but my understanding of the code base is too limited to make any further claims, so the rest of the blog post is focused on OpenJDK. Now over to the problems for profiler writers:
Problems
The fact that jmethodIDs are dynamically allocated in resizable caches causes major issues: Common profilers, like async-profiler, use AsyncGetCallTrace, as stated in the beginning. ASGCT is used inside signal handlers where obtaining a lock is unsupported. So the profiler has to ensure that every method that might appear in a trace (essentially every method) has an allocated jmethodID before the profiling starts. This leads to significant performance issues when attaching profilers to a running JVM. This is especially problematic in OpenJDK 8:
[…] the quadratic complexity of creating new jmethodIDs during class loading: for every added jmethodID, HotSpot runs a linear scan through the whole list of previously added jmethodIDs trying to find an empty slot, when there are usually none. In extreme cases, it took hours (!) to attach async-profiler to a running JVM that had hundreds thousands classes: https://github.com/async-profiler/async-profiler/issues/221
Andrei Pangin, developer of Async-Profiler
A jmethodID becomes invalid when its defining class is unloaded. Still, there is no way for a profiler to know when a jmethodID becomes invalid or even get notified when a class is unloaded. So processing a newly observed jmethodID and obtaining the name, signature, modifiers, and related class, should be done directly after obtaining the id. But this is impossible as all accessor methods allocate memory and thereby cannot be used in signal handlers directly after AsyncGetCallTrace invocations.
As far as I know, methods can be unloaded concurrently to
the native code executing JVMTI functions. This introduces a potential race
condition where the JVM unloads the methods during the check->use flow,
making it only a partial solution. To complicate matters further, no method
exists to confirm whether ajmethodID
is valid.Theoretically, we could monitor the
CompiledMethodUnload
event to track
the validity state, creating a constantly expanding set of unloadedjmethodID
values or a bloom filter, if one does not care about few
potential false positives. This strategy, however, doesn’t address the
potential race condition, and it could even exacerbate it due to possible
event delays. This delay might mistakenly validate ajmethodID
value that
has already been unloaded, but for which the event hasn’t been delivered
yet.Honestly, I don’t see a way to use
Jaroslav Bachorik ON ThE OpenJDK MailingListjmethodID
safely unless the code using
it suspends the entire JVM and doesn’t resume until it’s finished with thatjmethodID
. Any other approach might lead to JVM crashes, as we’ve
observed with J9.
(Concurrent) class unloading, therefore, makes using all profiling APIs inherently unsafe.
jclass ids suffer from the same problems, but ses, we could just process all jmethodIDs and jclass ids, whenever a class is loaded and store all information on all classes, but this would result in a severe performance penalty, as only a subset of all methods actually appears in the observed traces. This approach feels more like a hack.
While jmethodIDs are pretty helpful for other applications like writing debuggers, they are unsuitable for profilers. As I’m currently in the process of developing a new profiling API, I started looking into replacements for jmethodIDs that solve all the problems mentioned before:
Solution
My solution to all these problems is ASGST_Method and ASGST_Class, replacements for jmethodID and jclass, with signal-safe helper methods and a proper notification mechanism for class, unloads, and redefinitions.
The level of indirection that jmethodID offers is excellent, but directly mapping ASGST_Method to method objects removes the problematic dynamic jmethodID allocations. The main disadvantage is that class redefinitions cause a method to have a new ASGST_Method id and a new ASGST_Class id. We solve this the same way JFR solves it:
We use a class local id (idnum) for every method and a JVM internal class idnum, which are both redefinition invariant. The combination of class and method idnum (cmId) is then a unique id for a method. The problem with this approach is that mapping a cmId to an ASGST_Method or a method object is prohibitively expensive as it requires the JVM to check all methods of all classes. Yet this is not a problem in the narrow space of profiling, as a self-maintained mapping from a cmId to collected method information is enough.
The primary method for getting the method information, like name and signature, is ASGST_GetMethodInfo
in my proposal:
// Method info // You have to preallocate the strings yourself // and store the lengths in the appropriate fields, // the lengths are set to the respective // string lengths by the VM, // be aware that strings are null-terminated typedef struct { ASGST_Class klass; char* method_name; jint method_name_length; char* signature; jint signature_length; char* generic_signature; jint generic_signature_length; jint modifiers; jint idnum; // class local id, doesn't change with redefinitions jlong class_idnum; // class id that doesn't change } ASGST_MethodInfo; // Obtain the method information for a given ASGST_Method and // store it in the pre-allocated info struct. // It stores the actual length in the *_len fields and // a null-terminated string in the string fields. // A field is set to null if the information is not available. // // Signal safe void ASGST_GetMethodInfo(ASGST_Method method, ASGST_MethodInfo* info); jint ASGST_GetMethodIdNum(ASGST_Method method);
The similar ASGST_Class related is ASGST_GetClassInfo
:
// Class info, like the method info typedef struct { char* class_name; jint class_name_length; char* generic_class_name; jint generic_class_name_length; jint modifiers; jlong idnum; // id, doesn't change with redefinitions } ASGST_ClassInfo; // Similar to GetMethodInfo // // Signal safe void ASGST_GetClassInfo(ASGST_Class klass, ASGST_ClassInfo* info); jlong ASGST_GetClassIdNum(ASGST_Class klass);
Both methods return a subset of the information available through JVMTI methods. The only information missing that is required for profilers is the mapping from method byte-code index to line number:
typedef struct { jint start_bci; jint line_number; } ASGST_MethodLineNumberEntry; // Populates the method line number table, // mapping BCI to line number. // Returns the number of written elements // // Signal safe int ASGST_GetMethodLineNumberTable(ASGST_Method method, ASGST_MethodLineNumberEntry* entries, int length);
All the above methods are signal safe so the profiler can process the methods directly. Nonetheless, I propose conversion methods so that the profiler writer can use jmethodIDs and jclass ids whenever needed, albeit with the safety problems mentioned above:
jmethodID ASGST_MethodToJMethodID(ASGST_Method method); ASGST_Method ASGST_JMethodIDToMethod(jmethodID methodID); jclass ASGST_ClassToJClass(ASGST_Class klass); ASGST_Class ASGST_JClassToClass(jclass klass);
The last part of my proposal deals with invalid class and method ids: I propose a call-back for class unloads, and redefinitions, which is called shortly before the class and the method ids become invalid. In this handler, the profiler can execute its own code, but no JVMTI methods and only the ASGST_* methods that are signal-safe.
Remember that the handler can be executed concurrently, as classes can be unloaded concurrently. Class unload handlers must have the following signature:
void ASGST_ClassUnloadHandler(ASGST_Class klass, ASGST_Method *methods, int count, bool redefined, void* arg);
These handlers can be registered and deregistered:
// Register a handler to be called when class is unloaded // // not signal and safe point safe void ASGST_RegisterClassUnloadHandler( ASGST_ClassUnloadHandler handler, void* arg); // Deregister a handler to be called when a class is unloaded // @returns true if handler was present // // not signal and safe point safe bool ASGST_DeregisterClassUnloadHandler( ASGST_ClassUnloadHandler handler, void* arg);
The arg
parameter is passed directly to the handler as context information. This is due to the non-existence of proper closures or lambdas in C.
You might wonder we my API would allow multiple handlers. This is because a JVM should support multiple profilers at once.
Conclusion
jmethodIDs are unusable for profiling and cause countless errors, as every profiler will tell you. In this blog post, I offered a solution I want to integrate into the new OpenJDK profiling API (JEP 435). My proposal provides the safety that profiler writers crave. If you have any opinions on this proposal, please let me know. You can find a draft implementation can be found on GitHub.
See you next week with a blog post on safe points and profiling.
This project is part of my work in the SapMachine team at SAP, making profiling easier for everyone. Thanks to Martin Dörr, Andrei Pangin, and especially Jaroslav Bachorik for their invaluable input on my proposal and jmethodIDs.