Java Heap Analysis

Memory leaks are fairly common. If you have one, typically you’ll see the JVM used memory rising over a period of time. Finding out what is using this memory can be quite tricky.

In general the easiest way to do this is to use the JMAP tool to create a ‘heap dump’. Repeat that process and then compare the two heap dumps to see what is ‘rising’ in memory.

What follows is a piece of Java code that does exactly that, and writes out files to /tmp (or wherever you choose) showing which objects are using memory over time.

This code is a quick and dirty hack, but it should show you how you can do this yourself.


package net.richardsenior.java;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LeakFinder {
	private static final Logger LOGGER = LoggerFactory.getLogger(LeakFinder.class);
	private Timer timer;
	private AtomicReference<Map<String,Integer>>previous = new AtomicReference<Map<String,Integer>>();
	private AtomicReference<Map<String,Integer>>offenders = new AtomicReference<Map<String,Integer>>();

	public LeakFinder() {
		timer = new Timer();
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				LOGGER.info("about to process heap memory usage information..");
				process();
			}
		},60000,300000);
	}

	private static String exec(String[] command) {
		try {
			Process p = Runtime.getRuntime().exec(command);
			p.waitFor(1,TimeUnit.MINUTES);
			BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
			String ret = IOUtils.toString(stdInput);
			return ret;
		} catch (Throwable t) {}
		return "";
	}

	/**
	 * perform operations async
	 */
	public void process() {
		CompletableFuture.runAsync(() -> this.doProcess());
	}

	private void doProcess() {
		Map<String,Integer>current = getHeapHistogram();
		if (current==null) {
			return;
		}
		Map<String,Integer>prev = previous.get();
		if (prev==null) {previous.set(current);return;}

		//calculate how much growing things have grown
		Map<String,Integer>off = new HashMap<String,Integer>();
		for (Map.Entry<String,Integer>p:prev.entrySet()) {
			Integer curr = current.get(p.getKey());
			if (curr==null) continue;
			if (curr > p.getValue()) {
				off.put(p.getKey(),curr - p.getValue());
			}
		}
		this.offenders.set(off);
		this.previous.set(current);

		StringBuilder s = new StringBuilder();
		//dump out to file
		for (Map.Entry<String,Integer>o:off.entrySet()) {
			s.append(o.getValue());
			s.append(",");
			s.append(o.getKey());
			s.append("\n");
		}
		String fn = "" + System.currentTimeMillis() + ".dmp";
		BufferedWriter bwr=null;
		try {
			bwr = new BufferedWriter(new FileWriter(new File("/tmp/" + fn)));
			bwr.write(s.toString());
			bwr.flush();
			bwr.close();
		} catch (Throwable t) {
			LOGGER.warn("failed to write memory stats to file in /tmp");
		} finally {
			try {if (bwr!=null) {bwr.close();}} catch (Throwable t) {}
		}
	}

	private Map<String,Integer> getHeapHistogram() {
		try {
			String pid = exec(new String[] {"/bin/sh","-c","/usr/bin/jps | grep start | sed 's/[^0-9]*//g'"});
			String histo = exec(new String[] {"/bin/sh","-c","/usr/bin/jmap -histo " + pid});
			Pattern p = Pattern.compile(" *(\\d+)\\: +(\\d+) +(\\d+) {2}([^\\n]+)",Pattern.DOTALL);
			Matcher m = p.matcher(histo);
			Map<String,Integer>h = new HashMap<String,Integer>();
			while (m.find()) {
				Integer num = null;
				try {num=Integer.parseInt(m.group(3));} catch (Throwable t) {}
				if (num==null) continue;
				h.put(m.group(4),num);
			}
			return h;
		} catch (Throwable t) {}
		return null;
	}
}