Memory profiling – simple examples

Recently I have been trying to learn different memory profiling tools to monitor Java applications. I have looked into the command line tools that are shipped as part of JDK like jstat, jps, jvisualvm etc. Licensed tools like Yourkit provide wholesome information about a running JVM including memory usage, CPU time, thread count etc. Running a java application with -verbose:gc option prints memory usage of each generation after every garbage collection event.

Profiler tools like VisualVM modify the bytecode of the application they are profiling, which may result in some peculiarities. For example, if we profile the following code in VisualVM, it shows heap usage increasing linearly over time even though no objects are being created. We can verify this with the following example:

public class EmptyWhileLoop {
  public static void main(String[] arg) {
    while(true) {}
  }
}

VisualVM output

Alternatively, jstat can provide us periodical information on a wide range of parameters like garbage collection statistics, heap capacities, class compilation etc.

Typically jstat can be used in following way:

jstat <option> <pid> <interval> <count>

So if we run the following on command prompt,

jstat -gcutil 410 500 10

It will print garbage collection statistics of the JVM running with pid 410 every 0.5 second, 10 times. If we don’t give the count option, jstat will run till the process exits. pid of an application can be found using jps command.

For above code, jstat -util will give the output as following:

jstat output

The meaning of each column can be found in the official Oracle documentation.

Now let’s consider a simple example of creating an ArrayList with and without providing initialCapacity. If we do not provide initialCapacity to the ArrayList constructor, by default it creates an internal array of initial size 10. As we add elements to this ArrayList and exceed this size, a new internal array with size as 1.5 times the old size is created and values from old array are copied to the new one. If we add large number of values to an ArrayList but do not initialize it, the internal array will have to be resized several times over, and the discarded array objects go up for garbage collection. So an application that does not initialize ArrayList is going to consume much more memory than the one that does. We can verify this by running the JVM with -verbose:gc option.

Suppose we have a class like this:

import java.util.*;

public class ArrayListProfiler {
  public static void main(String[] a) {
    List<String> array = new ArrayList<String>();
    
    for(int i=0; i<10000; i++) {
      array.add(String.valueOf(i));
    }
  }
}

I will compile and run it as following:

javac ArrayListProfiler.java && java -Xmx1m -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:D:\gc.log ArrayListProfiler

First let’s understand what each of these options means. -Xmx1m sets the maximum heap size for this application as 1MB. Limiting the heap size to some small value is important to simulate low memory condition. Otherwise, JVM won’t need to kick in the garbage collector. -verbose:gc enables logging of garbage collection information. -XX:+PrintGCDetails and -XX:+PrintGCTimeStamps give detailed GC information and generation wise heap usage. -Xloggc:D:\gc.log logs the -verbose:gc output to a file.

The output generated for above code is as following:

Java HotSpot(TM) 64-Bit Server VM (25.40-b25) for windows-amd64 JRE (1.8.0_40-b26), built on Mar  7 2015 13:51:59 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8365480k(1878384k free), swap 16729064k(4152084k free)
CommandLine flags: -XX:InitialHeapSize=1048576 -XX:MaxHeapSize=1048576 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
0.077: [GC (Allocation Failure) [PSYoungGen: 512K->496K(1024K)] 512K->520K(1536K), 0.0007685 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.080: [GC (Allocation Failure) [PSYoungGen: 1008K->486K(1024K)] 1032K->806K(1536K), 0.0009741 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.081: [Full GC (Ergonomics) [PSYoungGen: 486K->486K(1024K)] [ParOldGen: 320K->288K(512K)] 806K->774K(1536K), [Metaspace: 2439K->2439K(1056768K)], 0.0064944 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 1024K, used 517K [0x00000000ffe80000, 0x0000000100000000, 0x0000000100000000)
  eden space 512K, 6% used [0x00000000ffe80000,0x00000000ffe87bc0,0x00000000fff00000)
  from space 512K, 95% used [0x00000000fff80000,0x00000000ffff9a28,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 512K, used 288K [0x00000000ffe00000, 0x00000000ffe80000, 0x00000000ffe80000)
  object space 512K, 56% used [0x00000000ffe00000,0x00000000ffe481c8,0x00000000ffe80000)
 Metaspace       used 2445K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 266K, capacity 386K, committed 512K, reserved 1048576K

A detailed explanation of how to read this -verbose:gc output can be found here. But for our purpose, we can see that there were three garbage collection events and old generation space usage is more than 50%.

Now let’s initialize the ArrayList object and run the same test.

import java.util.*;

public class ArrayListProfiler {
  public static void main(String[] a) {
    List<String> array = new ArrayList<String>(10000);

    for(int i=0; i<10000; i++) {
      array.add(String.valueOf(i));
    }
  }
}

The output in this case comes out to be like this:

Java HotSpot(TM) 64-Bit Server VM (25.40-b25) for windows-amd64 JRE (1.8.0_40-b26), built on Mar  7 2015 13:51:59 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8365480k(1929064k free), swap 16729064k(4231388k free)
CommandLine flags: -XX:InitialHeapSize=1048576 -XX:MaxHeapSize=1048576 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
0.071: [GC (Allocation Failure) [PSYoungGen: 512K->487K(1024K)] 512K->503K(1536K), 0.0008072 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 1024K, used 907K [0x00000000ffe80000, 0x0000000100000000, 0x0000000100000000)
  eden space 512K, 82% used [0x00000000ffe80000,0x00000000ffee9250,0x00000000fff00000)
  from space 512K, 95% used [0x00000000fff00000,0x00000000fff79c50,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 512K, used 16K [0x00000000ffe00000, 0x00000000ffe80000, 0x00000000ffe80000)
  object space 512K, 3% used [0x00000000ffe00000,0x00000000ffe04000,0x00000000ffe80000)
 Metaspace       used 2445K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 266K, capacity 386K, committed 512K, reserved 1048576K

Garbage collection was needed only once and old generation space usage is only 3% because unlike the previous case, the internal arrays didn’t have to be recreated.

Java 

See also