Running on Java 24-ea+24-2960 (Preview)
Home of The JavaSpecialists' Newsletter

142Instrumentation Memory Counter

Author: Dr. Heinz M. KabutzDate: 2007-03-26Java Version: 5Category: Performance
 

Abstract: Memory usage of Java objects has been a mystery for many years. In this newsletter, we use the new instrumentation API to predict more accurately how much memory an object uses. Based on earlier newsletters, but revised for Java 5 and 6.

 

Welcome to the 142nd edition of The Java(tm) Specialists' Newsletter, sent to you from Europe. In most countries in Europe, it seems that the state has taken on an overly protective role over its citizens. This constrasts starkly with Africa, where you are given lots of opportunities to permanently remove yourself from the gene pool. Having grown up in Africa, I find nanny states oppressive. This is one of the reason we chose to live on the island of Crete. You are still allowed to live as a thinking human being and exercise your God-given common sense. For example, when construction is done on a road, they only close half the road. No detours necessary! This means that both directions share one lane, together with the huge earth-moving machines occuping part of the lane at times. Like in a computer game, you need to time your passing of the giant machines or face pulverization. Incredibly, no one gets hurt. (Ok, I am exaggerating a little :-))

Peter von der Ahé wrote to me after last week's newsletter that the enum hack is also prevented in Java 5 if we run it with the -Xfuture option. Probably a good idea if we need to use Java 5. I tried some other hacks, but none of them worked.

javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.

Instrumentation Memory Counter

A few years ago, I described memory usage in Java. This was further expanded in MemoryCounter for Java 1.4, which applied my algorithm using reflection. After Java 5 came out, I kept on receiving emails telling me about the new instrumentation interface. Unfortunately none of the emails gave a clue on how to use it. The instrumentation interface gives you a flat size of the object and does not recursively estimate the size of the objects that it references.

In January, Maxim Zakharenkov published a blog describing how to combine instrumentation with my reflection based approach from newsletter#29. Maxim has kindly agreed to let me republish his approach in The Java(tm) Specialists' Newsletter. I have refactored his code a little bit and added support for well known shared flyweights. Since sending out the newsletter, I have been made aware of a project by Marco Rosi and Nicola Santi to write a sizeOf method for Java. It uses some similar approaches, but does not currently have support for detecting known shared flyweights.

One of the motivations of relooking at the Java 5 and 6 editions in respect of memory usage was an email sent to me by Shai Almog, a faithful subscriber. Shai is working on the Bean Properties for Java project. To demonstrate memory usage, Shai used my Memory Counter to estimate memory usage of his bean framework. Most of the figures that my Java 1.4 memory counter estimated are still correct for Java 5 and 6. But, I needed to revise it and get to the best estimates possible.

Let's go back to the steps necessary to use the instrumentation mechanism in Java 5 to estimate memory usage.

We can create an instrumentation agent in Java by writing a class containing a premain(String, Instrumentation) method. This method is called by the JVM on startup and an instance of Instrumentation is passed in. In the MemoryCounterAgent class, we keep a handle to the Instrumentation handle in a static variable.

We need to package the agent class into a JAR file and specify the Premain-Class in the manifest:

Premain-Class: eu.javaspecialists.tjsn.memory.MemoryCounterAgent

When we start the Java application, we need to specify the JVM parameter -javaagent pointing it to the JAR file. If we call the JAR file memoryagent.jar, we would need to start our class like this:

java -javaagent:lib/memoryagent.jar <Our main class>

Here is an ANT build script, which assumes your sources are in the src directory:

    <?xml version="1.0"?>
    <project name="memoryagent" default="compile">
      <target name="init">
        <tstamp/>
        <mkdir dir="build"/>
      </target>

      <target name="compile" depends="init">
        <javac srcdir="src" source="1.5" target="1.5"
               destdir="build"/>
        <copy todir="build/META-INF">
          <fileset dir="src/META-INF"/>
        </copy>
        <jar jarfile="memoryagent.jar" basedir="build"
             filesetmanifest="merge"/>
      </target>

      <target name="clean">
        <delete dir="build"/>
        <delete file="memoryagent.jar"/>
      </target>
    </project>    

The details of how the reflection code in our MemoryCounterAgent works, is described in newsletter#78. The one thing I would like to add to Maxim's version is to increase the detection ability of Java 5 flyweights. In Java 1.4, the only flyweights we worried about were interned Strings. These we sniffed out by comparing the string handle to string.intern(). However, we should also think about the Boolean.TRUE and Boolean.FALSE flyweights, plus all the Integer object flyweights that are obtained during autoboxing with Integer.valueOf(int). We also eliminate enum instances from the count. To reduce the number of comparisons that we need to make, we first check whether the object is Comparable, since all the flyweights implement that interface.

package eu.javaspecialists.tjsn.memory;

import java.lang.instrument.Instrumentation;
import java.lang.reflect.*;
import java.util.*;

public class MemoryCounterAgent {
  private static Instrumentation instrumentation;

  /** Initializes agent */
  public static void premain(String agentArgs,
                             Instrumentation instrumentation) {
    MemoryCounterAgent.instrumentation = instrumentation;
  }

  /** Returns object size. */
  public static long sizeOf(Object obj) {
    if (instrumentation == null) {
      throw new IllegalStateException(
          "Instrumentation environment not initialised.");
    }
    if (isSharedFlyweight(obj)) {
      return 0;
    }
    return instrumentation.getObjectSize(obj);
  }

  /**
   * Returns deep size of object, recursively iterating over
   * its fields and superclasses.
   */
  public static long deepSizeOf(Object obj) {
    Map visited = new IdentityHashMap();
    Stack stack = new Stack();
    stack.push(obj);

    long result = 0;
    do {
      result += internalSizeOf(stack.pop(), stack, visited);
    } while (!stack.isEmpty());
    return result;
  }

  /**
   * Returns true if this is a well-known shared flyweight.
   * For example, interned Strings, Booleans and Number objects
   */
  private static boolean isSharedFlyweight(Object obj) {
    // optimization - all of our flyweights are Comparable
    if (obj instanceof Comparable) {
      if (obj instanceof Enum) {
        return true;
      } else if (obj instanceof String) {
        return (obj == ((String) obj).intern());
      } else if (obj instanceof Boolean) {
        return (obj == Boolean.TRUE || obj == Boolean.FALSE);
      } else if (obj instanceof Integer) {
        return (obj == Integer.valueOf((Integer) obj));
      } else if (obj instanceof Short) {
        return (obj == Short.valueOf((Short) obj));
      } else if (obj instanceof Byte) {
        return (obj == Byte.valueOf((Byte) obj));
      } else if (obj instanceof Long) {
        return (obj == Long.valueOf((Long) obj));
      } else if (obj instanceof Character) {
        return (obj == Character.valueOf((Character) obj));
      }
    }
    return false;
  }

  private static boolean skipObject(Object obj, Map visited) {
    return obj == null
        || visited.containsKey(obj)
        || isSharedFlyweight(obj);
  }

  private static long internalSizeOf(
      Object obj, Stack stack, Map visited) {
    if (skipObject(obj, visited)) {
      return 0;
    }

    Class clazz = obj.getClass();
    if (clazz.isArray()) {
      addArrayElementsToStack(clazz, obj, stack);
    } else {
      // add all non-primitive fields to the stack
      while (clazz != null) {
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
          if (!Modifier.isStatic(field.getModifiers())
              && !field.getType().isPrimitive()) {
            field.setAccessible(true);
            try {
              stack.add(field.get(obj));
            } catch (IllegalAccessException ex) {
              throw new RuntimeException(ex);
            }
          }
        }
        clazz = clazz.getSuperclass();
      }
    }
    visited.put(obj, null);
    return sizeOf(obj);
  }

  private static void addArrayElementsToStack(
      Class clazz, Object obj, Stack stack) {
    if (!clazz.getComponentType().isPrimitive()) {
      int length = Array.getLength(obj);
      for (int i = 0; i < length; i++) {
        stack.add(Array.get(obj, i));
      }
    }
  }
}

No newsletter would be complete without a test case. Let's write a class that constructs a number of objects and gets both the shallow and deep sizes.

package eu.javaspecialists.tjsn.memory.test;

import eu.javaspecialists.tjsn.memory.MemoryCounterAgent;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MemoryCounterAgentTest {
  public static void measureSize(Object o) {
    long memShallow = MemoryCounterAgent.sizeOf(o);
    long memDeep = MemoryCounterAgent.deepSizeOf(o);
    System.out.printf("%s, shallow=%d, deep=%d%n",
        o.getClass().getSimpleName(),
        memShallow, memDeep);
  }
  public static void main(String[] args) {
    measureSize(new Object());
    measureSize(new HashMap());
    measureSize(new LinkedHashMap());
    measureSize(new ReentrantReadWriteLock());
    measureSize(new byte[1000]);
    measureSize(new boolean[1000]);
    measureSize(new String("Hello World".toCharArray()));
    measureSize("Hello World");
    measureSize(10);
    measureSize(100);
    measureSize(1000);
    measureSize(new Parent());
    measureSize(new Kid());
    measureSize(Thread.State.TERMINATED);
  }

  private static class Parent {
    private int i;
    private boolean b;
    private long l;
  }

  private static class Kid extends Parent {
    private boolean b;
    private float f;
  }
}

When we run this, we see the following output:

    Object, shallow=8, deep=8
    HashMap, shallow=40, deep=120
    LinkedHashMap, shallow=48, deep=160
    ReentrantReadWriteLock, shallow=24, deep=104
    byte[], shallow=1016, deep=1016
    boolean[], shallow=1016, deep=1016
    String, shallow=24, deep=64
    String, shallow=0, deep=0
    Integer, shallow=0, deep=0
    Integer, shallow=0, deep=0
    Integer, shallow=16, deep=16
    Parent, shallow=24, deep=24
    Kid, shallow=32, deep=32
    State, shallow=0, deep=0

For primitive arrays, the shallow and deep results are equal. The flyweights have a size of 0.

Performance

The instrumentation approach produces slightly more accurate results than my memory counter from a few years ago. However, my memory counter appears to be mostly faster than the instrumentation approach.

An optimization could be to cache known class object sizes. Any class that contains only primitive fields, will always produce objects of the same size. For example, Integer will always be 16 bytes (assuming it is not a shared flyweight).

It should also be noted that the instrumentation interface only provides an estimated memory usage. From my experiments it appears that memory usage might differ depending on the garbage collector used. Since it is possible to change GCs at runtime (if I recall correctly), it also means that the memory usage requirement of an object might change at runtime.

For more information about the Flyweight Pattern, why not attend our Design Patterns Course?

Kind regards

Heinz

 

Comments

We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...