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;
}
}