Instrumentation: querying the memory usage of a Java object
The most reliable— but not necessarily the easiest— way to estimate the
usage of a Java object is to ask the JVM. Querying the JVM for the memory
usage of an object requires the Instrumentation framework introduced in Java 5.
Hotspot supports the instrumentation framework; if you use a VM from a different vendor, it will
need to provide similar support, and there may be some variation in the procedures described here.
The general idea is:
- we create a special agent class with a special premain method, generally
compiling it against the rest of our code so that it can see the definitions of the classes
whose memory usage we're interested in measuring;
- the JVM passes a special Instrumentation object to our premain method,
which we can use to query the size of an object;
- we package the compiled agent class into a jar with a special manifest file;
- we run our program, passing in the agent jar to the VM command line arguments.
On the rest of this page, we'll go through the above procedure step by step.
1. Creating the instrumentation agent class
An instrumentation agent is a class with a special method, with a predefined signature, that the
JVM will invoke before the rest of the application for you to set up any instrumentation
code. Generally, instrumentation is the act of transforming classes for profiling
purposes: for example, we could manipulate the definition of the String class to increment
a counter every time a string is created, and thus measure e.g. how many strings per second our
application creates. But an interesting additional feature provided by the instrumentation framework is
the ability to measure the memory usage of an object.
Our agent is simply a class with the following method defined:
public static void premain(String args, Instrumentation inst) {
...
}
The JVM will pass to our method an implementation of the Instrumentation interface,
defined in java.lang.instrument. In turn, this interface
defines the method getObjectSize(). So for example, if we want to measure the memory usage
of an instance of SomeClass, our agent code would look as follows:
import java.lang.instrument.*;
import com.somepackage.SomeClass;
public class MyAgent {
public static void premain(String args, Instrumentation inst) {
SomeClass obj = new SomeClass();
long size = inst.getObjectSize(obj);
System.out.println("Bytes used by object: " + size);
}
}
Note that there's no interface that our agent needs to define: we just need to make sure that
we get the method signature correct so that the JVM will find it.
2. Package the agent into a jar
Once we have compiled our agent class, we need to package it into a jar. This step is
slightly fiddly, because we also need to create a manifest file. The latter
is simple a text file containing a single line that specifies the agent class. For example,
you can create a file called manifest.txt with the following line:
Premain-Class: mypackage.MyAgent
Then, to create the jar, we execute the following command (it's usually worth creating
a batch file or shell script with this line in case you need to re-build the agent jar
several times):
jar -cmf manifest.txt agent.jar mypackage/MyAgent.class
3. Run the application with the agent
Now, we execute the application as usual, but use the javaagent command line
argument to specify that we want to attach our instrumentation agent. For example, assuming the classpath
is the current directory and that the application's main method is in
com.mypackage.Main:
java -javaagent:agent.jar -cp . com.mypackage.Main
Now, before our application is run, our agent's premain method will be run. And
in this case, the size of an instance of SomeClass will be created.
Accessing the Instrumentation object from within our application
In our simple example above, we measured the size of an object created during the
execution of the premain method. But what if we want to measure the size of an object
during the lifetime of the application? Well, we can still do so; we just
need to arrange for the rest of our application to see the Instrumentation object
passed to the premain method. This works because the single agent class (and the
Instrumentation instance) created at startup remains valid throughout the application.
So we can simple store the Instrumentation instance in a static variable, then
provide a static method to access it. For example:
public class MyAgent {
private static volatile Instrumentation globalInstr;
public static void premain(String args, Instrumentation inst) {
globalInstr = inst;
}
public static long getObjectSize(Object obj) {
if (globalInstr == null)
throw new IllegalStateException("Agent not initted");
return globalInstr.getObjectSize(obj);
}
}
Now, provided the agent is included in the JVM command line parameters as above,
then from anywhere in our application we can call MyAgent.getObjectSize() to
query the memory size of an object created by our Java application.
Calculating "deep" memory usage of an object
Note that the getObjectSize() method does not include the memory used by
other objects referenced by the object passed in. For example, if Object A
has a reference to Object B, then Object A's reported memory usage will include only
the bytes needed for the reference to Object B (usually 4 bytes),
not the actual object.
To get a "deep" count of the memory usage of an object (i.e. which includes
"subobjects" or objects referred to by the "main" object), then you can use
the Classmexer agent available for beta download from
this site.
If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.
Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.