by Kim Levesque
We want to hear from you! Please send us your
FEEDBACK.
The following technical article may contain actual software programs in source
code form. This source code is made available for developers to use as needed,
pursuant to the terms and conditions of this license.
(May, 2003)
Introduction
The Java Virtual Machine
Profiling Interface (JVMPI) provides a means for profiling an
application to better understand its memory allocation, CPU
utilization, lock contention and more. This is accomplished with
a "profiler agent" a native library that is dynamically
loaded upon Java Virtual Machine (JVM) startup, which can receive information about specific
events that occur within the JVM. The
profiler agent must obtain a reference to the JVMPI interface, which
can then be used to register for particular events within the JVM, and can
extract additional information about those events. This interface
is described in greater detail at
http://java.sun.com/j2se/1.4.1/docs/guide/jvmpi/jvmpi.html#overview.
Background
While it is still only an
experimental feature, the JVMPI has been incorporated since the Java
Development Kit (JDK) 1.1 release, and is the foundation for several
commercial Java profilers which are currently available. hprof
is a profiler which is included as part of the standard JDK. However,
developers may find that the existing profilers do not provide them
with the specific information they need. In the area of heap
allocation, for example, the existing profilers are primarily focused
on tracking memory leaks. They can provide very useful information
about the number of object instances that have been created, along
with the overall size of the heap. Nevertheless, it is difficult to
extract information about where and when an object of a particular
size has been created, which may be necessary in order to track down
large memory allocations.
For
example, hprof gives heap information in the following format when
the program ends (from
http://java.sun.com/j2se/1.4.1/docs/guide/jvmpi/jvmpi.html#hprof-heap):
Command
used: javac
-J-Xrunhprof:heap=sites foo.java ...
percent
live alloc'ed stack class
rank self accum
bytes objs bytes objs trace name
1 9.18% 9.18% 149224 5916 1984600 129884 1073
char []
...
SITES END
SITES BEGIN
(ordered by live bytes) Wed Oct 7 11:38:10 1998
From
this output we can tell the total amount of char[]
that have been allocated (1,984,600 bytes), and the amount that are
currently live, that is, not garbage collected (149,224 bytes). We can
then match the stack trace number (1073) with the stack information
provided by hprof.
...
TRACE 1073: (thread=1)
java/lang/String.(String.java:244)
sun/tools/java/Scanner.bufferString(Scanner.java:143)
sun/tools/java/Scanner.scanIdentifier(Scanner.java:942)
sun/tools/java/Scanner.xscan(Scanner.java:1281)
THREAD
START (obj=1d6b20, id = 1, name="main", group="main")
Unfortunately, this gives no information about the individual allocation events, such as the point at which they occurred or how large they were. To get this type of information, it is necessary to write a custom profiler. This is a straightforward task, however, few examples have been published. This article steps through the process of writing a profiler to collect this information.
The
entry point for the JVM into the profiler agent is the JVM_OnLoad
method. There are three specific tasks which must be done here.
First, a reference to a JVMPI_Interface must be obtained using the
GetEnv function call. The JVMPI interface contains pointers
to all of the interface functions implemented by the JVM, such as
EnableEvent, GetCurrentThreadCPUTime, RunGC,
etc. which are described in the "Interface Functions"
section of the JVMPI documentation.
Second,
the profiler must register its interest in particular events by using
the EnableEventfunction. The
full list of events that the profiler can register for is shown in
the JVMPI documentation. For this large memory allocation profiler,
EnableEvent must be called to register for event notification,
both when objects are allocated (the JVMPI_EVENT_OBJECT_ALLOC event)
and when classes are loaded (the JVMPI_EVENT_CLASS_LOAD event). The
class loading event is needed in order to obtain class names and
method names for filling in the stack trace.
Finally,
a function pointer to the NotifyEvent
implementation must be set up in JVM_OnLoad. NotifyEvent
is the only function in the interface which needs to be implemented
by the author of the profiler agent. This is the method that will be
called by the JVM on the profiler agent when an event has occurred
that the profiler has registered interest in.
These three tasks are shown in the following code, after the necessary includes and variable declarations.
---Start
of allocprof.cc---
---continued
below---
#include <stdlib.h>
#include <string.h>
#include <list>
#include "jvmpi.h"
#include "jni.h"
#define DEPTH 5 //depth of stack trace
#define ALLOC_LIMIT 5000 //object size for notification
#define FALSE 0
#define TRUE 1
using namespace std;
// global jvmpi interface pointer
static JVMPI_Interface *jvmpi_interface;
//Class_Info stores information about a loaded class
class Class_Info {
public:
char *name;
list<JVMPI_Method> methods;
jint num_methods;
jobjectID class_id;
} ;
list<Class_Info> classes; //all loaded classes
list<Class_Info>::iterator c_iter; //iterator for classes
list<JVMPI_Method>::iterator m_iter; //iterator for methods
Class_Info a_class;
JVMPI_Method a_method;
// profiler agent entry point
extern "C" {
JNIEXPORT jint JNICALL JVM_OnLoad(JavaVM *jvm, char *options, void *reserved) {
// get jvmpi interface pointer
if ((jvm->GetEnv((void **)&jvmpi_interface, JVMPI_VERSION_1)) < 0) {
printf("Error obtaining jvmpi interface pointer\n");
return JNI_ERR;
}
// enable event notification
jvmpi_interface->EnableEvent(JVMPI_EVENT_OBJECT_ALLOC, NULL);
jvmpi_interface->EnableEvent(JVMPI_EVENT_CLASS_LOAD, NULL);
// initialize jvmpi interface
jvmpi_interface->NotifyEvent = notifyEvent;
return JNI_OK;
}
}
The
next step is to implement the NotifyEvent
method. This method will be called by the JVM with a JVMPI_Event
object parameter when the event occurs. The JVMPI_Event
object contains a great deal of useful information about the event.
All fields of the JVMPI_Event structure are described in the
"Events" section of the JVMPI documentation. The
JVMPI_EVENT_CLASS_LOAD event is described by:
struct {
char *class_name;
char *source_name;
jint num_interfaces;
jint num_methods;
JVMPI_Method *methods;
jint num_static_fields;
JVMPI_Field *statics;
jint num_instance_fields;
JVMPI_Field *instances;
jobjectID class_id;
} class_load;
For
this profiler, the class ID, class name, number of methods, and the
pointer to the JVMPI_Method
objects are important. For each method in the class, the method
name and method id are also obtained from the corresponding
JVMPI_Method object. All of this information is stored in the
Class_Info object defined above, and then this Class_Info
instance is added to the classes list.
---continued
from above---
// function for handling event notification
void notifyEvent(JVMPI_Event *event) {
int x, found;
switch(event->event_type) {
/* -----------------------------------------------------
* when class is loaded, store method and class
* information in "classes" list so that this info
* can be printed in the stack trace
--------------------------------------------------- */
case JVMPI_EVENT_CLASS_LOAD:
a_class.class_id = event->u.class_load.class_id;
a_class.name = strdup(event->u.class_load.class_name);
a_class.num_methods = event->u.class_load.num_methods;
for(int x = 0; x < a_class.num_methods; x ++) {
a_method.method_name = strdup(event->u.class_load.methods[x].method_name);
a_method.method_id = event->u.class_load.methods[x].method_id;
a_class.methods.push_back(a_method);
}
classes.push_back(a_class);
return;
---continued below---
The
JVMPI_EVENT_OBJECT_ALLOC event is described by:
struct {
jint arena_id;
jobjectID class_id;
jint is_array;
jint size;
jobjectID obj_id;
} obj_alloc;
In
the notifyEvent function below,
when an event is received with an event_type of
JVMPI_EVENT_OBJECT_ALLOC, we first check the size to
see if the number of bytes is greater than the limit set for
ALLOC_LIMIT at the start of the program. If so, the JVMPI interface
function, GetCallTrace, is used to obtain the JVMPI_CallFrame
objects at a stack depth specified by DEPTH. The JVMPI_CallFrame
objects only contain the method id and the line number, which is why
getting the class and method names that correspond to method ids in
the step above is important. Now one must simply iterate through the
loaded classes and for each class, iterate through the methods of
that class. When the matching method id is found, a meaningful stack
trace can be printed which represents the location at which the large
object was allocated.
---continued
from above---
/* -------------------------------------------------
* When object is allocated, check if size is over
* ALLOC_LIMIT. If so, print stack trace.
----------------------------------------------- */
case JVMPI_EVENT_OBJECT_ALLOC:
if (event->u.obj_alloc.size > ALLOC_LIMIT) {
printf("\nLarge object allocated: size %d\n", event->u.obj_alloc.size);
//get stack trace
JVMPI_CallTrace trace;
trace.frames = (JVMPI_CallFrame *) malloc(sizeof(JVMPI_CallFrame) * DEPTH);
trace.env_id = event->env_id;
jvmpi_interface->GetCallTrace(&trace, DEPTH);
//iterate through loaded classes and their methods to
//find a match on the trace method_id
for (x = 0; x < trace.num_frames; x++) {
found = FALSE;
c_iter = classes.begin();
while ((c_iter != classes.end()) && (found != TRUE)) {
m_iter = c_iter->methods.begin();
while ((m_iter != c_iter->methods.end()) && (found != TRUE)) {
if (trace.frames[x].method_id == m_iter->method_id) {
printf("at line %d in method %s in class %s\n", trace.frames[x].lineno, m_iter->method_name, c_iter->name);
found = TRUE;
}
++m_iter;
}
++c_iter;
}
}
free(trace.frames);
}
return;
}
}
---End
of allocprof.cc---
This library can be built on a UNIX® platform with a C++ compiler using the command:
CC
-G -I. -I$(JDK_HOME)/include -I$(JDK_HOME)/include/solaris
allocprof.cc -o liballocprof.so
The
LD_LIBRARY_PATH environment variable must be set to the directory
that contains liballocprof.so. The profiler will be loaded by the
JVM if the -Xrunproflib option is used when starting the Java
application:
java
-Xrunallocprof <class file>
and the output will resemble:
at
line 280 in method <init> in class
java.util.jar.Manifest$FastInputStream
at
line 275 in method <init> in class
java.util.jar.Manifest$FastInputStream
at
line 158 in method read in class java.util.jar.Manifest
at
line 52 in method <init> in class java.util.jar.Manifest
at
line 148 in method getManifest in class java.util.jar.JarFile<{>
Large
object allocated: size 800016
at
line 12 in method <init> in class Glist
at
line 33 in method main in class Garbage ....
Large
object allocated: size 8208
Please
note that the JVMPI was not included with the production versions of
1.2.2_xx for the Solaris Operating Environment, only the reference implementations. The
jvmpi.h file must be present in the $JDK_HOME/include directory for
the JVMPI to be used. Finally, profiling an application will
decrease performance and should only be used in a testing
environment. However, if used properly, the JVMPI can be an
invaluable tool for application profiling.
JavaTM Virtual Machine Profiler Interface (JVMPI)
Comprehensive Profiling Support in the Java Virtual Machine, Sheng Liang and Deepa Viswanathan
DOC ID# 1900