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:

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.