As Java developers, we are familiar with our applications throwing OutOfMemoryErrors or our server monitoring tools throwing alerts and complaining about high JVM memory utilization.

To investigate a memory problem, you usually start by looking at THE JVM heap memory.

To do this, we can trigger the program to throw OutOfMemoryError and then capture the heap dump. Next, we will analyze the heap dump to identify potential objects that could cause memory leaks.

Code sample

Examples of working code on GitHub are attached to this article.

What is a Heap dump?

Whenever we create a Java object by creating an instance of a class, it is always placed in an area called the heap. The Java runtime classes are also created in the heap.

The heap is created when the JVM starts. It expands or contracts at run time to accommodate objects created or destroyed in our application.

When stacked, the garbage collection process runs to collect objects that are no longer referenced (that is, the program no longer uses them). More information about memory management can be found in the Oracle documentation.

The heap dump contains snapshots of some instances of live objects (note: live objects in heap memory) that are currently being used by the Java application. We can get detailed information about each object instance, such as address, type, class name, or size, and whether the instance has references to other objects.

Heap dumps come in two formats:

  • The Classic Format
  • The Portable Heap Dump (PHD) format

PHD is the default format. The classical format is human-readable because it is ASCII text, but the PHD format is binary and should be processed with the appropriate tools for analysis.

A sample program that generates OutOfMemoryError

To explain heap dump analysis, we’ll use a simple Java program to generate OutOfMemoryError:

public class OOMGenerator {

  / * * *@param args
   * @throws Exception 
   */
  public static void main(String[] args) throws Exception {
    
    System.out.println("Max JVM memory: " + Runtime.getRuntime().maxMemory());
    try {
      ProductManager productManager = new ProductManager();
      productManager.populateProducts();
      
    } catch (OutOfMemoryError outofMemory) {
      System.out.println("Catching out of memory error");
   
      throwoutofMemory; }}}Copy the code
public class ProductManager {
  private static ProductGroup regularItems = new ProductGroup();

  private static ProductGroup discountedItems = new ProductGroup();

  public void populateProducts(a) {

    int dummyArraySize = 1;
    for (int loop = 0; loop < Integer.MAX_VALUE; loop++) {
      if(loop%2= =0) {
        createObjects(regularItems, dummyArraySize);
      }else {
        createObjects(discountedItems, dummyArraySize);
      }
      System.out.println("Memory Consumed till now: " + loop + "... ""+ regularItems + ""+discountedItems );
      dummyArraySize *= dummyArraySize * 2; }}private void createObjects(ProductGroup productGroup, int dummyArraySize) {
    for (int i = 0; i < dummyArraySize; ) { productGroup.add(createProduct()); }}private AbstractProduct createProduct(a) {
        int randomIndex = (int) Math.round(Math.random() * 10);
        switch (randomIndex) {
          case 0:
            return  new ElectronicGood();
          case 1:
            return  new BrandedProduct();
          case 2:
            return new GroceryProduct();
          case 3:
            return new LuxuryGood();
          default:
            return  newBrandedProduct(); }}}Copy the code

We keep allocating memory by running a for loop until we reach a point where the JVM does not have enough memory to allocate, causing an OutOfMemoryError to be thrown.

Find the root cause of OutOfMemoryError

We will now find out the cause of this error through heap dump analysis. This is done in two steps:

  • Capture heap dump
  • Analyze heap dump files to locate suspicious causes.

We can capture heap dumps in a number of ways. Let’s first capture the heap dump of our example using JMap, then pass a VM parameter on the command line.

Generate heap dumps on demand using JMAP

The JMap tool is packaged with the JDK and extracts the heap dump to the specified file location.

To use JMAP to generate a heap dump, we first use the JPS tool to find the process ID of our running Java program to list all running Java processes on our machine:

After running the JPS command, we can see that the processes are listed in “” format.

Next, we run the jmap command to generate the heap dump:

jmap -dump:live,file=mydump.hprof 41927
Copy the code

After running this command, a heap dump file with the extension hprof is created.

Option -dump:live is used to collect only live objects that are still referenced in running code. When the Live option is used, a full GC is triggered to clean up unreachable objects, and then only the referenced live objects are dumped.

Automatically generates a heap dump on OutOfMemoryErrors

This option is used to automatically capture heap dumps when an OutOfMemoryError occurs. This helps diagnose the problem because we can see which objects are in memory and what percentage of memory they occupied when an OutOfMemoryError occurred.

We’ll use this option in our example, because it gives us more insight into the cause of the crash.

Let us from the command line or our favorite IDE using VM options HeapDumpOnOutOfMemoryError born to run the program with a heap dump file:

Java - jar target/oomegen - 0.0.1 - the SNAPSHOT. Jar \ - XX: + HeapDumpOnOutOfMemoryError \ - XX: HeapDumpPath = hdump. HprofCopy the code

After running our Java program with these VM parameters, we get the following output:

Max JVM memory: 2147483648 Memory Consumed till now: 960 Memory Consumed till now: 29760 Memory Consumed till now: 25949760 Java. Lang. OutOfMemoryError: Java heap space Dumping heap to/hdump hprof... Heap dump file created [17734610 bytes in 0.031 secs] Catching out of memory error Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at io.pratik.OOMGenerator.main(OOMGenerator.java:25)Copy the code

As you can see from the output, a heap dump file named hdump.hprof is created when OutOfMemoryError occurs.

Other ways to generate a heap dump

Some other ways to generate a heap dump are:

  1. JCMD: JCMD is used to send diagnostic command requests to the JVM. It is packaged as part of the JDK. It can be found in the \bin folder of the Java installation.
  2. JVisualVM: In general, analyzing a heap dump requires more memory than the actual heap dump size. This can be problematic if we try to analyze heap dumps from large servers on a development machine. JVisualVM provides real-time sampling of heap memory, so it does not occupy the entire memory.

Analyzing the Heap Dump

What we are looking for in a heap dump is:

  • Objects with high memory usage
  • An object graph for identifying objects that have not freed memory
  • Reachable and unreachable objects

Eclipse Memory Analyzer (MAT) is one of the best tools for analyzing Java heap dumps. Let’s understand the basic concepts for Java heap dump analysis using MAT by analyzing the heap dump files we generated earlier.

We’ll start by launching the memory profiler tool and opening the heap dump file. In Eclipse MAT, two types of object sizes are reported:

  • Shallow heap size: The Shallow heap of an object is its size in memory
  • Retained heap size: The Retained heap is the amount of memory that is released when an object is garbage collected.

The overview section in MAT

Once the heap dump is open, we see an overview of the application’s memory usage. The pie chart shows the largest objects by reserved size in the Overview TAB, as shown below:

For our application, the information in the overview means that if we could handle a specific instance of java.lang.Thread, we would save 1.7 GB, and almost all of the memory used in the application.

Histogram view

While this seems promising, java.lang.Thread is unlikely to be the real problem here. To get a better idea of what objects currently exist, we’ll use the histogram view:

We use the regular expression “io.pratik.*” to filter the histogram to show only the classes that match the pattern. From this view, we can see the number of live objects: for example, there are 243 BrandedProduct objects and 309 Price objects in the system. We can also see the amount of memory used by each object.

There are two types of calculations, Shallow heap and Retained heap. Shallow heap is the amount of memory consumed by an object. For each reference, the object needs 32 (or 64 bits, depending on the architecture). Primitives like integers and long integers require 4 or 8 bytes, and so on… While this might be interesting, a more useful metric is the retention heap.

Retained Heap Size

The reserved heap size is calculated by adding the sizes of all objects in the reserved set. The reserved X set is the set of objects that the garbage collector will delete when it collects X.

The reserved heap can be calculated in two different ways, using quick approximations or precise retention sizes:

By calculating the reserved heap, we can now see that io.pratik.productGroup takes up most of the memory, even though it is only 32 bytes itself (shallow heap size). By finding a way to free this object, we can certainly control our memory problems.

Dominator Tree

The dominance tree is used to identify reserved heaps. It is generated from a complex object graph generated at run time and helps identify the largest memory graph. Object X is said to dominate object Y if every path from root to Y must go through X.

Looking at the dominance tree for our example, we can see which objects are kept in memory.

We can see that the ProductGroup object holds memory instead of Thread objects. We may be able to solve the memory problem by releasing the objects contained within this object.

A Leak Suspects Report

We can also generate “leak suspect reports” to look for suspected large objects or sets of objects. This report displays the findings on an HTML page and is also stored in a ZIP file next to the heap dump file.

Because of its small size, it is better to share the Leak Suspicious Report report with a team dedicated to performing analysis tasks rather than the original heap dump file.

The report has a pie chart that gives the size of the suspect object:

For our example, we flagged a suspect problem and further described it with a short description:

In addition to the summary, this report contains detailed information on suspected issues, which can be accessed through the “Details” link at the bottom of the report:

Details include:

  1. Shortest path from GC root to cumulative point: Here we can see all the classes and fields that the chain of references passes through, which gives a good understanding of how objects are held. In this report, we can see the chain of references from Thread to ProductGroup object.
  2. Cumulative objects in the dominant tree: This provides some information about the cumulative content, which is a collection of GroceryProduct objects here.

conclusion

In this article, we introduced heap dumps, which are snapshots of the memory map of Java application runtime objects. To illustrate this, we captured a heap dump from a program that throws OutOfMemoryError at run time.

Then we looked at some of the basic concepts for heap dump analysis using Eclipse Memory Analyzer: large objects, GC roots, shallow and reserved heaps, and domination trees, all of which will help us determine the root cause of a particular Memory problem.