Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

What Is the Java VM Profiler Interface?


Sep99: Java Q&A

Andy is the author of Network Programming with Visual J++ 6.0 and a software developer at Compuware-NuMega lab. You can contact him at [email protected].


The Java Virtual Machine Profiler Interface (JVMPI) lets you build tools that collect events about the state of the virtual machine. Events contain information about class loading, object allocation, method calls, and more. Consequently, these events give you a way to gather some interesting information about how the VM and your Java application actually run.

The JVMPI provides fundamental and useful information, but all the information is in the form of events. The JVMPI does not produce any information in a user presentable sort of way. Say you wanted to get the total amount of memory allocated by the VM when running a given Java application. There is no single call in the JVMPI to get the total amount of memory allocated. Instead, you collect a series of object allocation events and add up the sizes of all the objects. When the application terminates, you can display the results of your calculations. The important point is that much of the information from the JVMPI is in a raw format, requiring you to write additional code to organize the events into useful information.

Collecting event information from the JVMPI appears straightforward enough. However, you quickly run into issues such as thread synchronization, deadlocks, and crashes due to requesting events at the wrong time. Finally, the JVMPI is new to the JDK 1.2, so it only works with Java Platform 2-compliant virtual machines.

Using the JVMPI

The JVMPI is not a package or magical Java library. Instead, it is a type of hooking mechanism. This hooking mechanism requires you to write modules that get loaded into the VM. These modules are libraries written in some native language like C or C++. Consequently, your JVMPI module is platform specific.

There are several high-level steps involved with creating and using JVMPI modules. Thankfully, these modules all have the same requirements regardless of the platform.

The first step is creating the library's entry point. All JVMPI modules must export a function called JVM_OnLoad(), which receives several parameters -- a pointer to the Java environment, argument list, and reserved pointer. The VM calls this method so that the JVMPI library can register for a series of different event types.

Before registering for an event, you must:

  • Confirm that you have the right version of the VM. Since the JDK 1.2 is the first environment to support the JVMPI, there shouldn't be any compatibility issues. All the VMs that support the JVMPI should have the same core event list.
  • Get a pointer to the VM's JVMPI environment. The JVMPI environment is a pointer to a very important set of function pointers. The library must reassign the JVMPI NotifyEvent() function pointer. The NotifyEvent() function pointer is the event-handler function. The VM calls the NotifyEvent() function with the new event. Consequently, you can't get any event without reassigning this function pointer.

Calling the Java environment's GetEnv() function performs both steps. The method checks the version of the VM and returns a pointer to the JVMPI environment. There are two distinct environment pointers:

  • A Java VM environment object of type JavaVM.
  • The JVMPI, which is of type JVMPI_Interface. The JavaVM object is primarily responsible for getting a pointer to the JVMPI. The JVMPI provides all the functionality for the enabling and requesting of events, as well as event processing through the NotifyEvent() function.

Event processing is similar to processing Windows messaging on Windows. You implement a function with a particular signature. This function contains a switch-case construct where each case represents a particular event type.

To make up the boilerplate code that every JVMPI library uses, you must:

1. Implement a function called JVM_OnLoad(). This function is responsible for getting the JVMPI environment, assigning the NotifyEvent() function pointer, and registering events.

2. Implement the function responsible for receiving the events.

From this point on, everything relates to handling events. Event processing is somewhat easy once you understand a few fundamental concepts.

JVMPI Events

The NotifyEvent() function has only one parameter, a JVMPI_Event pointer. The JVMPI_Event structure contains members that represent the event type, the event-specific data, and a pointer to the JNI environment. Listing One is a simplified version of the JVMPI_Event structure.

The event_type member obviously represents the type of event this object contains. This member carries two important pieces of information. The first is the actual event type. The second is the creator of the event.

There are two ways to register for an event:

  • By enabling the event in the JVM_OnLoad() function by calling the JVMPI_Interface->EnableEvent() function, which instructs the VM to send a given event to the NotifyEvent() function each time an event of that type occurs.
  • By calling the JVMPI_Interface->RequestEvent(). The RequestEvent() tells the VM to send the next occurrence of a given event type to the NotifyEvent() function. Only a few events can be requested through RequestEvent(). The JVMPI documentation specifies what those events are. The VM distinguishes events generated by EnableEvent() and RequestEvent(). Events objects generated by RequestEvent() have the most significant bit of event_type enabled.

  • Consequently, if you use RequestEvent, the event handler must mask off the MSB of the event_type to get the correct event type. Otherwise, your event-handling code will not interpret the event type correctly.

The next member is the env_id, which is a pointer into the JNI environment for the thread the Java application is running in. This member lets you call all the JNI functions, allowing you to interact with the VM.

The last member is a union. There is a different unnamed structure for each event type. To interpret any event, you must examine the event_type field to figure out which unnamed structure to interpret to get the information you are interested in.

This covers the JVMPI_Event structure as well as the notion that there are two different events -- enabled and requested events. Events may also fall into one of three categories: defining, undefining, and generic events. The JVMPI_Event object does not represent these types in any particular way. Only the JVMPI documentation specifies the relationship of events and categories.

The event categories provide you with information about lifetime and location. A defining event represents the beginning of a lifetime, while an undefining event tells us when the lifetime is ending. A generic message contains nonlifetime-specific information. However, the event handler can treat all messages as generic as long as you only need to collect the event and not do any specific processing.

The notion of a lifetime is important when discussing heaps, classes, or objects. If a library tries to invoke a static method from a class with a lifetime that hasn't begun (for example, the class hasn't been loaded), the VM will crash. The library can manipulate or examine objects when the lifetime is current, not prior to or after the lifetime. Depending upon the operation, it may be important to track lifetime, while in other situations you simply don't care.

It isn't enough to simply deal with defining and undefining events. Event processing has an important trap. Any instance of a VM may have several threads. Each thread can send the JVMPI library events. Consequently, the library must be concerned with two important issues -- deadlock and mutual exclusion. The VM may attempt to allocate resources across threads. This allocation or locking process can lead to thread deadlocks. The documentation defines several cases that can cause deadlock. It is easy for any multithreaded application to corrupt its own data. Therefore, the library must implement some mutual exclusion mechanism.

The final concern is when to request an event. It's important to understand that certain things must occur before the library can request certain types of events. Say, for example, that the library tries to get an object dump after processing the allocation event of an object. Requesting an object dump at this point between allocation and initialization will cause the VM to fault. Therefore, you must always understand the state of the event and what it implies before you try to request another event that may provide more details about the current event.

ProfilerHook

The ProfilerHook.dll is a sample library that tracks three import Java events. The first event is JVMPI_EVENT_CLASS_LOAD. This defining event provides the library with a list of all classes the VM loads. Each time the VM loads and defines a new class, the VM will send this event. Using this information, the library can build a table of class names.

The second event is JVMPI_EVENT_OBJECT_ALLOC. The VM sends this defining event each time the VM allocates memory for an instance of a class. However, the VM does not assign the values of the object until after the event processing. Attempting to manipulate the object while it is between allocated and defined states is unsafe and results in a fault. The importance of the event is that it provides the library with the amount of memory allocated for the particular class instance.

The interesting thing about the JVMPI_ EVENT_OBJECT_ALLOC event is that the results for each instance of the same class are identical. Every instance of a given class allocates the same amount of memory. Listing Two shows a sample class. When the VM allocates memory for a class, it begins by allocating four bytes (or the size of a native pointer). This first 4-byte block represents a pointer for the first member field. From this point on, the VM will allocate in 8-byte increments (or two native pointers) to contain the next one or two fields. If the Foo class had two fields, the VM would allocate 12 bytes. If the Foo class had three fields (as it does), the VM would still allocate 12 bytes. If the Foo class had four fields, the VM would allocate 20 bytes. Note that this allocation is type independent and does not represent the memory allocated to the field itself. The memory for a given class instance represents the memory necessary to represent all the references to the fields, not the fields themselves. Consequently, the memory allocation on a per-class basis is identical. It also means the event does not represent the total cumulative memory allocated to an object and all its fields.

The final event this library uses is JVMPI_EVENT_JVM_SHUT_DOWN. This undefining event represents the termination of the VM. The VM sends this event before shutting down. The JVMPI documentation does not mark this event as an undefining event, but clearly it specifies when the VM is closing, and thus the undefining of everything that occurred in this run. This event is useful for doing final cleanup, but is also a good spot for the ProfilerHook library to dump its results.

Listing Three shows much of the ProfilerHook library's code. The library uses three global variables. The g_pProfiler represents the JVMPI profiler interface. The g_pProfiler pointer gives the application access to many different functions necessary to work within the VM. It's handy to keep this pointer around for the duration of the application. The next global is a Windows critical-section object. This object is a convenient way of enforcing mutual exclusion. The last global represents a collection of classes, the number of instances, and the total memory allocated for the class.

The first step in the library is some simple initialization of the global variables. The DllMain first determines if the VM is loading the library and initializes both the class map and the critical section. At termination, it releases the critical section.

The next step is to initialize the profiler interface and register for the events. The VM automatically calls the JVM_OnLoad function. The function then gets the JVMPI interface and confirms that the library is running in a compatible version of the VM. Next, the library reassigns the NotifyEvent() function with the library's own HandleEvent(). The library will now receive events. The last operation the function performs is to register the events the library will need to collect memory information. There you see g_pProfiler->EnableEvent() with the various event types. The VM will now send events to the library.

The HandleEvent() function is straightforward enough. The function begins by stripping off the requested event bit so the application can always process a given event correctly. Next, the HandleEvent() function enters the critical section. It is necessary to have some mutual exclusion mechanism present since the library may receive many threads simultaneously. The event drops into a switch-case construct that decodes the events and updates the various tables accordingly. Finally, when the application finishes, the library displays the statistics to the console.

There are a few more steps necessary to get the ProfilerHook library to run. The first step is to copy the ProfilerHook library into the Java home directory's bin directory. The next step is to instruct the VM to load the hook. The Java 1.2 VM has an -X command line switch. The user would simply specify java -XrunProfilerHook <class name>. The -Xrun tells the VM to run some library, while the ProfilerHook is the name of the library itself. Alternatively, you can also specify the same options by creating a new environment variable called _JAVA_OPTIONS and set it equal to the -XrunProfilerHook. This would look like _JAVA_OPTIONS= -XrunProfilerHook.

The results of using this library (available electronically; see "Resource Center," page 5) can be staggering. Listing Four is a simple HelloWorld application. The first time I tested the application, the VM allocated more than a thousand string objects, while my application only uses one string. Running against larger, more complex Java applications, is almost frightening. However, consider that the Java class library is huge and depends on many support classes. The JVMPI does not distinguish between user code and Java core code. Consequently, the JVMPI sends you information about everything that happens in the Java application. Another important item to note is that the library uses only three of dozens of events that are available in the JVMPI.

Conclusion

Errors in Java code are often easier to debug than bugs in other languages. The reason is that Java code is straightforward. However, the problems shift from programmatic errors to errors in design or performance issues. Using the JVMPI, you can quickly build a small tool capable of providing some insight as to why an application is behaving slowly or allocating massive chunks of memory.

DDJ

Listing One

/** JVMPI_Event -- Taken and modified from JVMPI.h */
typedef struct {
    jint event_type;                  /* event_type */
    JNIEnv *env_id;                   /* env where this event occured */
    union {
        struct {
        char *class_name;         /* class name */
        char *source_name;        /* name of source file */
        jint num_interfaces;      /* number of interfaces implemented */
        jint num_methods;         /* number of methods in the class */
        JVMPI_Method *methods;    /* methods */
        jint num_static_fields;   /* number of static fields */
        JVMPI_Field *statics;     /* static fields */
        jint num_instance_fields; /* number of instance fields */
        JVMPI_Field *instances;   /* instance fields */
        jobjectID class_id;       /* id of the class object */
    } class_load;
    /* There are many more event data structures. */
    } u;
} JVMPI_Event;

Back to Article

Listing Two

public class Foo
{
    int a;
    Object b;
    Object c;

    public static void main( String[] args )
    {
        new Foo();
    }
}

Back to Article

Listing Three

// ProfilerHook.cpp
// globals
JVMPI_Interface*    g_pProfiler;        // Global profiler interface ref    
CRITICAL_SECTION    g_criticalSection;  // mutual exclusion mechanism
CLASSMAP*           g_pClasses;         // stores the class list

BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, 
                                                   LPVOID lpReserved )
{
    if ( ul_reason_for_call == DLL_PROCESS_ATTACH )
    {
        // initialize the critical section and the class map
        g_pClasses = new CLASSMAP();
        InitializeCriticalSection ( &g_criticalSection );
    } else if ( ul_reason_for_call == DLL_PROCESS_DETACH )
    {  
        DeleteCriticalSection( &g_criticalSection ); 
    }
    return TRUE;
}
extern "C" JNIEXPORT jint JNICALL JVM_OnLoad( JavaVM *jvm, 
                                       char *options, void *reserved)
{    
    // Get profiler interface and confirm you 
        //         have a compatible version of the VM
    int res = jvm->GetEnv((void**)&g_pProfiler, JVMPI_VERSION_1);
    if ( res < 0 )
        return JNI_ERR;     // error result
    // Reassign the profiler NotifyEvent function so we receive events
    g_pProfiler->NotifyEvent = HandleEvent;
    
    /** Register the events you want to receive. You need events for class
        * load, object allocation and shutdown. Class load gives you a mapping
        * between allocated object and class type. Shutdown lets you know when
        * the VM is closing and when to dump your information */
    g_pProfiler->EnableEvent ( JVMPI_EVENT_CLASS_LOAD, NULL ); 
    g_pProfiler->EnableEvent ( JVMPI_EVENT_OBJECT_ALLOC, NULL ); 
    g_pProfiler->EnableEvent ( JVMPI_EVENT_JVM_SHUT_DOWN, NULL ); 
    
    return JNI_OK;          // success result
}     
extern "C" void HandleEvent( JVMPI_Event *pEvent )
{
    int event = pEvent->event_type & 0xffff;
    // Enter a critical section since everything must be thread safe
    EnterCriticalSection( &g_criticalSection );
    switch ( event )
    {
    case JVMPI_EVENT_CLASS_LOAD:
        {   
            /* Store the class so that we can colect memory information */
            ClassItem* pItem = new ClassItem(pEvent);
            g_pClasses->insert( CLASSVALUE( 
                                 pEvent->u.class_load.class_id, pItem ) ); 
        }
        break;
    case JVMPI_EVENT_OBJECT_ALLOC:

        {    
            /* Increment memory allocated to a class */
            CLASSMAPITERATOR it = 
                           g_pClasses->find( pEvent->u.obj_alloc.class_id );
            if ( it != g_pClasses->end() )
            {   
                ClassItem* pItem = (*it).second;
                pItem->count++;
                pItem->size += pEvent->u.obj_alloc.size;
            }
        }
        break;
    case JVMPI_EVENT_JVM_SHUT_DOWN:
        {
            /* Sort and dump the memory results */
            long totalObjects = 0;
            long totalMemory = 0;
            cout << "\n\nShutdown Summary" << endl;
            cout << "Size\tCount\tName" << endl;

            CLASSMAPITERATOR it = g_pClasses->begin();
                        
            vector<ClassItem*> *list = new vector<ClassItem*>();
            vector<ClassItem*>::iterator listIt;

            while ( it != g_pClasses->end() )
            {    
                ClassItem* pItem = (*it).second;
    
                list->push_back(pItem);
    
                totalObjects += pItem->count;
                totalMemory += pItem->size; 

                it++;
            }
            sort( list->begin(), list->end(), ClassItemGreater() );
            listIt = list->begin();

            while ( listIt != list->end() )
            {    
                ClassItem* pItem = (*listIt);
                cout << pItem->size << "\t" << pItem->count 
                    << "\t" << pItem->szClassName << endl;
                listIt++;
            }
            cout << endl;
            cout << "Total Memory:\t" << totalMemory << endl;
            cout << "Total Objects:\t" << totalObjects << endl;
            g_pClasses->clear();
            delete g_pClasses;
        }
        break;
    }
    // Leave the critical section
    LeaveCriticalSection( &g_criticalSection );
}

Back to Article

Listing Four

class HelloWorld {
    public static void main(String[] args ){
        System.out.println( "Hello World!" );
    }
}

Back to Article


Copyright © 1999, Dr. Dobb's Journal

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.