Offline Memory Profiling

March 10th, 2009, 3:35 pm by Mathias

I think I took care of the DefinitionsPane memory leak that we’ve seen occasionally in our tests during the last years (actually, the last 4 years, 9 months, judging by the repository). The NULL\_DOCUMENT instance in AbstractDJPane.java had listeners, and those listeners held references to the DefinitionsPane or the OpenDefinitionsDocument.

For those of you who aren’t that familiar with the code base, the NULL\_DOCUMENT is an example of the “null object design pattern”. There is the notion of a “current document”, but in some cases, e.g. during initialization, there may not be a current document. We could set the field for the current document to null, but then we would have to check for null everywhere the field is used. That’s not very robust. Instead, we created an instance of a document, called NULL\_DOCUMENT, that isn’t really used as a document at all. It’s just used to signal that there is no current document, but to act graciously if the field is used.

The document still had listeners, though, even though it should really just be a dummy. It should be as much a dummy as null, without the side effects of a NullPointerException. After tracing the problem to NULL\_DOCUMENT (more on how i did that below), I overrode the addXXXListener methods for NULL\_DOCUMENT to be no-ops. Now listeners just cannot be added to this instance. The memory leak was only intermittent, so it’s difficult to say for sure that the problem has been remedied, but I’ve run 100 repetitions of the DefinitionsPaneTest and not seen the problem again.

I want to discuss a little bit how I spotted this problem: There are two tools that Java provides, jmap and jhat (really starting with JDK6; unfortunately not supported on the Mac). jmap dumps the entire heap of a JVM into a file. jhat then analyzes that file and provides a HTML interface to browse the results. For more information, look at this article: Finding Memory Leaks in Java Apps.

The biggest problem here was that the problem wasn’t visible all the time, and when we noticed it (because the unit test failed), it was really too late already to execute jmap to retrieve a heap dump. So I wrote a dumpHeap() method that executes jmap automatically, from within the program; that way, I can dump it only if the memory leak has been detected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
  /** Dumps the current heap to a file. */
  public static File dumpHeap() throws IOException, InterruptedException {
    String javaHome = System.getenv("JAVA_HOME");
    char SEP = File.separatorChar;
   
    // try jps first
    File jps = new File(javaHome+SEP+"bin"+SEP+"jps");
    // if that doesn't work, try jps.exe
    if (!jps.exists()) jps = new File(javaHome+SEP+"bin"+SEP+"jps.exe");
   
    // execute jps
    ProcessBuilder pb = new ProcessBuilder(jps.getAbsolutePath());
    LOG.log(java.util.Arrays.toString(pb.command().toArray()));
    Process jpsProc = pb.start();
    jpsProc.waitFor();
    LOG.log("jps returned "+jpsProc.exitValue());
   
    // read the output of jps
    BufferedReader br = new BufferedReader(new InputStreamReader(jpsProc.getInputStream()));
    Integer pid = null;
    String line = null;
    while((pid==null) && (line=br.readLine())!=null) {
      LOG.log(line);
      // find the PID of JUnitTestRunner, i.e. the PID of the current process
      if (line.indexOf("JUnitTestRunner")>=0) {
        pid = new Integer(line.substring(0,line.indexOf(' ')));
      }
    }
    if (pid==null) throw new FileNotFoundException("Could not detect PID");
    LOG.log("PID is "+pid);
   
    // try jmap first
    File jmap = new File(javaHome+SEP+"bin"+SEP+"jmap");
    // if that doesn't work, try jmap.exe
    if (!jmap.exists()) jmap = new File(javaHome+SEP+"bin"+SEP+"jmap.exe");
   
    // execute jmap -heap:format=b PID
    pb = new ProcessBuilder(jmap.getAbsolutePath(),
                            "-heap:format=b",
                            pid.toString());
    LOG.log(java.util.Arrays.toString(pb.command().toArray()));
    Process jmapProc = pb.start();
    jmapProc.waitFor();
    LOG.log("jmap returned "+jmapProc.exitValue());
   
    // read the output of jmap
    br = new BufferedReader(new InputStreamReader(jmapProc.getInputStream()));
    while((line=br.readLine())!=null) {
      LOG.log(line);
    }
   
    // rename the file
    File dump = new File("heap.bin");
    if (!dump.exists()) { throw new FileNotFoundException("heap.bin not found"); }
    File newDump = new File("heap-DefinitionsPaneTest-"+pid+"-"+System.currentTimeMillis()+".bin");
    dump.renameTo(newDump);
    return newDump;
  }

Then I still had to associate the instances of DefinitionsPane and OpenDefinitionsDocument with the instances reported in the HTML output. The contents of integer fields is displayed in the output, so I added a field that contains the identity hash code.

When I looked at the reference chains from the root set (which cannot be garbage-collected) to an instance of DefinitionsPane, I noticed that they all end with the following part:

--> class edu.rice.cs.drjava.ui.AbstractDJPane (84 bytes)  (static field NULL_DOCUMENT:)
--> edu.rice.cs.util.text.SwingDocument@0x72b4b840 (78 bytes) (field listenerList:)
--> javax.swing.event.EventListenerList@0x72b4d450 (12 bytes) (field listenerList:)
--> [Ljava.lang.Object;@0x74f52968 (136 bytes) (Element 23 of [Ljava.lang.Object;@0x74f52968:)
--> javax.swing.plaf.basic.BasicTextUI$UpdateHandler@0x74f6b578 (17 bytes) (field this$0:)
--> javax.swing.plaf.basic.BasicTextPaneUI@0x74f6b5b0 (29 bytes) (field editor:)
--> edu.rice.cs.drjava.ui.DefinitionsPane@0x74f67eb8 (559 bytes)

That means that if we can break this part of the chain, the DefinitionsPane can be deallocated. By not allowing listeners, that’s what I have done.

Print This Print This   Email This Email This

Leave a Reply

You must be logged in to post a comment.