In-place JVM memory analysis with jmap

4 minute read

Diagnosing a java application for memory leaks or heavy resource consumption may involve the use of GUI tools such as JConsole or VisualVM. However, jmap, a small JDK tool may be very helpful.

Using a command line tool allows to do a quick in-place diagnosis of the JVM in a headless environment. Jmap exists since JDK 5, is available on OpenJDK and is the perfect tool for this task.

Sample application to analyze

Let’s create a sample inspired from a situation I encountered in real life a few years ago.

The story

Imagine that you have a backend application that receives files from a third party and processes them.

The process:

  • When a file is received, a database transaction is created and some data are stored
  • For each line of the file, some process is done and some lines are stored (inside the current transaction)
  • Finally, at the end of the file, the transaction is committed (containing all the lines)

Of course, multiple file integrations can occur simultaneously and other tasks are performed in the application at the same time.

An additional specificity: the persistence framework used for this application stores entities in memory until the end of the transaction.

The codebase

Let’s look at our application.

The Entity object

We begin with the code for our Entity object (simplified to the extreme, not even a getter :)):

public class Entity {
	private String content;
	public Entity(String content) {
		this.content = content;
	}	
}

The FileProcessor object

Then, our demo program, simulating the transactionnal behaviour via a HashMap storing our entities.

import java.util.Map;
import java.util.HashMap;
import java.util.UUID;

import java.util.concurrent.TimeUnit;

public class FileProcessor {

    private int lineCount;
    private Map<Integer, Entity> transaction;

    public FileProcessor(int lineCount) {
        this.lineCount = lineCount;
    }

    public static void main(String[] args) throws InterruptedException {
        FileProcessor fp = new FileProcessor(1_000);
        // Simulate reading 1000 lines
        fp.launchProcess();
    }

    public void launchProcess() throws InterruptedException {
        initTransaction();

        for (int i = 0; i < lineCount; i++) {
            transaction.put(i, new Entity(UUID.randomUUID().toString())); 
        }

        commitTransaction();
    }

    private void initTransaction() {
        System.out.println(":: Beginning transaction");
        transaction = new HashMap<>();
    }

    private void commitTransaction() throws InterruptedException {
        System.out.println(":: Committing transaction");
        
        for (Entity e : transaction.values()) {
            TimeUnit.MILLISECONDS.sleep(1);
        }

        System.out.println(transaction.keySet().size() + " elements committed.");
        transaction = null;
    }
}

Our program simulates a 1000 lines files integration in database, the line number to simulate is defined at object creation: new FileProcessor(1_000).

The launchProcess() method starts by initializing the transaction (Map instanciation), then “loops on the lines” by creating the entities with a random unique id: UUID.randomUUID() and storing them in the transaction Map.

Finally, the transaction is committed with a delay of 1ms for each commit: TimeUnit.MILLISECONDS.sleep(1);

Here is the program log, displaying the number of created elements:

:: Beginning transaction
:: Committing transaction
1000 elements committed.

So far, so good.

Everything is looking fine and … one day … the trouble

Our business application runs fine for some months, other processes are written aside the file integration. One day, the application becomes slow as hell without any error log. Let’s say we pushed the number of lines to integrate to 1 million ;), replace the FileProcessor instanciation with:

FileProcessor fp = new FileProcessor(1_000_000);

Of course, you could investigate by debugging your code, adding logs or profiling, but using the right tool can give you a huge hint about the problem: jmap.

Identifying the process ID

There are two main ways to identify the process ID of the running VM you want to analyze.

The first one is to use the standard OS tools (such as the ‘ps’ command):

> ps -ef | grep java
gerben   9853  8112  0 15:42 pts/2    00:00:00 java FileProcessor
gerben   9958  8667  0 15:43 pts/1    00:00:00 grep --color=auto ...

The second one is to use the ‘jps’ command:

> jps
9865 Jps
9853 FileProcessor

In our case, we confirm that the PID is 9853.

jmap : Analyze the live objects in memory

Now that we have our process ID, we can use the jmap command with the -histo option. This option prints an “histogram” of the heap and the :live suboption asks only for live objects.

jmap -histo:live 9853 | head -n 15

 num     #instances         #bytes  class name
----------------------------------------------
   1:        400200       35208760  [C
   2:        398874       12763968  java.util.HashMap$Node
   3:        400182        9604368  java.lang.String
   4:        398886        6382176  java.lang.Integer
   5:        398758        6380128  Entity
   6:            34        4197984  [Ljava.util.HashMap$Node;
   7:           528          60656  java.lang.Class
   8:           436          28680  [I
   9:           542          27744  [Ljava.lang.Object;
  10:            15          26368  [B
  11:           341          10912  sun.misc.FDBigInteger
  12:           210           8400  java.util.LinkedHashMap$Entry

Here we can see that the [C class takes the vast majority of the VM memory (with the maximum instance number).

This class represents a characters array ([). So the main part of our VM memory is occupied by strings.

That’s confirmed by the following entries. In fact, if we look at the five first entries, we can conclude that our memory is saturated by Entity Objects and some HashMap nodes.

At this point, you should have enough hints to conclude that our transaction mechanism is taking the whole memory and find another solution to take care of your entities.

Conclusion

The aim of this post was to provide a quick introduction to the jmap JDK tool, especially the living objects histogram view that allows quick in-place diagnosis of the JVM memory.

Remember that you can use it when you’re logged on a headless server and don’t want to deploy heavy monitoring tools.

Categories: ,

Updated:


Written by

Gerben Castel

Father of 3, Software Architect, forever developer and tech enthusiast