Unlike C#, Java does not have a built-in sizeof operator that can return the memory (bytes) consumed by an object. You can however do-it-yourself. The steps are a bit involved and hence summarized below. Here is link to SO answer on same topic.
Create new Java Project
First create a new Java project with just a single class in it. We call it ObjectSizeFetcher. The code for ObjectSizeFetcher is below:
import java.lang.instrument.Instrumentation;
public class ObjectSizeFetcher {
private static Instrumentation instrumentation;
public static void premain(String args, Instrumentation inst) {
instrumentation = inst;
}
public static long getObjectSize(Object o) {
return instrumentation.getObjectSize(o);
}
}
instrumentation.getObjectSize will return the shallow object size of the argument passed to it. Make sure you understand the difference between shallow vs. deep so you are not caught by surprise.
Next in the pom.xml (the post assumes you are using Maven) add following section. Modify the packageName as needed. If ObjectSizeFetcher is defined in some package, you need to prefix the class name with the fully qualified package name in Premain-Class :
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifest>
<packageName>com.example</packageName>
</manifest>
<manifestEntries>
<Premain-Class>ObjectSizeFetcher</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
Also as in every pom.xml make sure you declare the group:artifactId:version of your assembly. E.g.:
<groupId>com.example</groupId>
<artifactId>instrumentation</artifactId>
<version>1.0-SNAPSHOT</version>
Now build the project using
$ mvn package
for example. This should generate a jar file under the target directory. Next, install this assembly in the local Maven repository. We do this by running:
$ mvn install
Tip: mvn install will also build the assembly if you haven’t built it already. So technically, you can just run mvn install and skip running mvn package.
Its a good idea now to verify that the jar file was indeed installed in Maven repository $HOME/.m2/repository.
Use the instrumentation jar to measure object sizes in your main app
Now we switch to the main application where you want to measure object sizes. First, you have to add a reference to the instrumentation jar that we built in previous step. Do that in pom.xml:
<dependency>
<groupId>com.example</groupId>
<artifactId>instrumentation</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
By installing the jar in the Maven repository in previous step (mvn install), Maven will now be able to find it in your main application when you try to build (compile) it.
Now just call ObjectSizeFetcher.getObjectSize to get the bytes consumed by an object in your application. ObjectSizeFetcher.getObjectSize works like the sizeof operator in C#.
You are now ready to run your main app. There is one gotcha. You can’t run your app in the traditional way. We have to use a javaagent. The Maven command will look like:
$ MAVEN_OPTS="-javaagent:$PWD/src/main/resources/instrumentation-1.0-SNAPSHOT.jar -enableassertions" \
mvn exec:java -Dexec.mainClass=my.main.Class -Dexec.cleanupDaemonThreads=false -Dexec.args="some argument"
Replace $PWD/src/main/resources/instrumentation-1.0-SNAPSHOT.jar with the full path where the instrumentation jar can be found. In my case I stored a copy of the jar in the src/main/resources folder of the main app.
If you followed the steps correctly, it should work.
Advanced
To measure the deep size of an object (we have to traverse the object graph for that) you can try using memory-measurer. There are couple of issues though. First, the project is built with Ant. You will need to convert to Maven (optional). Second and more importantly I couldn’t get it to work with Maven. The details are here. The problem is that this project uses dependencies (the instrumentation project we created has zero dependencies) and when using the mvn exec:java plugin to execute a Java application, the exec:java plugin does not push the classpath of dependencies to the javaagent. Let me know if you are able to get it to work.
Discussion
One cannot help but wonder why do we need to build a separate assembly and run it as a Java agent to measure memory usage? Why can’t we write a method that lives in the main application and returns memory usage?
My guess is that the instrumentation API by design is intended to be used to develop programs and applications (call it A) that run standalone and profile other applications (call it B) which act as input (to A) without messing source code of B. B is to be treated as a black-box application whose source code may not even be accessible. B may only be available as a pre-compiled jar file. So the normal (intended) use-case while using the instrumentation API is to call getObjectSize from A. The code in A would be calling getObjectSize to profile B. B is not supposed to access the instrumentation API. Calling the instrumentation API from B is a hacky way to get the object size. We don’t have any other way to get object size in Java so we leverage the hacky solution.
Btw if you think about it, its not that difficult to write a getObjectSize method that does not rely on the instrumentation API to measure memory consumed by an object. Use reflection to get the child objects and call getObjectSize on them recursively to calculate the total memory consumed. The implementation is left as an exercise for the reader.
Update 2023-9-7
Its 2023-9-7 and above steps no longer work on my new computer (Mac OS AArch64 w/ JDK-20) [1]. I tried both Oracle JDK and OpenJDK with no luck. I verified they still work on my old computer (Mac OS Intel x64 w/ JDK-17). Why? I don’t know. The only explanation I can think of is that maybe you shouldn’t be accessing java.lang.Instrumentation in your application code. If it works, its a bonus not something that is guaranteed. Another reason to hate Java. I was able to get around by instead using this code to calculate the object size. The caveat with this alternative is that it only works on the HotSpot VM (so it won’t work with OpenJDK for example). Another gotcha is that you might run into this exception (or something similar):
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field transient java.util.HashMap$Node[] java.util.HashMap.table accessible: module java.base does not "opens java.util" to unnamed module @31a07685
at java.lang.reflect.AccessibleObject.throwInaccessibleObjectException (AccessibleObject.java:387)
at java.lang.reflect.AccessibleObject.checkCanSetAccessible (AccessibleObject.java:363)
at java.lang.reflect.AccessibleObject.checkCanSetAccessible (AccessibleObject.java:311)
at java.lang.reflect.Field.checkCanSetAccessible (Field.java:181)
at java.lang.reflect.Field.setAccessible (Field.java:175)
at com.mycompany.app.ObjectSizeCalculator$ClassSizeInfo.<init> (ObjectSizeCalculator.java:404)
at com.mycompany.app.ObjectSizeCalculator.getClassSizeInfo (ObjectSizeCalculator.java:293)
at com.mycompany.app.ObjectSizeCalculator.visit (ObjectSizeCalculator.java:311)
at com.mycompany.app.ObjectSizeCalculator.calculateObjectSize (ObjectSizeCalculator.java:269)
at com.mycompany.app.ObjectSizeCalculator.getObjectSize (ObjectSizeCalculator.java:209)
The fix as I found after several hours of debugging is to add following flags to your MAVEN_OPTS or java command:
--add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED
Nothing ever works smoothly in Java.
Automatically opening core modules to the unnamed module
Chances are if you are using this code to calculate the object size, you will run into an endless series of
module xx does not opens xxx to unnamed module
You will fix one, then run into another and so on. Is there a way we could open everything? Luckily I found a solution here.
public static void openModulesForReflection() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException {
final Module unnamedModule = AddAllOpens.class.getClassLoader().getUnnamedModule();
final Method method = Module.class.getDeclaredMethod( "implAddExportsOrOpens", String.class, Module.class, boolean.class, boolean.class );
method.setAccessible( true );
ModuleLayer.boot().modules().forEach( module -> {
try {
final Set<String> packages = module.getPackages();
for( String eachPackage : packages ) {
method.invoke( module, eachPackage, unnamedModule, true, true );
log.info( "--add-open " + module.getName() + "/" + eachPackage + "=" + unnamedModule.toString() );
}
} catch( IllegalAccessException | InvocationTargetException e ) {
throw new RuntimeException( e );
}
} );
}
With this, you just have to --add-opens java.base/java.lang=ALL-UNNAMED on the command line. The other --add-opens will be added dynamically for you [1]. Give it a try.