Skip to main content

Writing Memory Leak Regression Tests with INSANE

Posted by timboudreau on April 17, 2005 at 11:49 AM PDT

A couple of years ago my friend Petr Nejedly created a handy library he calls INSANE. The reason for the name was, whenever he described the idea to people, they'd say "that's insane!" (but it is now also a cute acronym). What it does is static heap analysis - that is, it writes out every object in the JVM and how they reference each other. INSANE is also useful to diagnose OutOfMemoryErrors - just allocate some large array on application start (so you can clear enough room for INSANE to work after an OOME has happened), and install a handler for OutOfMemoryErrors which will clear the array and then dump the object graph of the JVM to a file. From there, grep is a handy tool to track down what's going on.

Download the source + library for this example here. Sources and more info on INSANE can be found here.

In NetBeans, we use INSANE for analyzing memory leaks; we also use it in our regression tests. Once you know you have a memory leak, you can use INSANE to print out exactly the reference graph that is causing the leak. So, when you fix a memory leak, you can write a unit test that will fail if the leak ever reappears. Some profilers can do this sort of thing too, but it's really nice to have a regression test that will tell you exactly what's going on without having to go in and profile the app.

A few people I've talked to about this here in Brazil have said "You mean you can have a memory leak in Java?" So we'll cover this quickly: A memory leak in Java happens when you allocate some object, and your code is now done with it, but it is still referenced somewhere, so the object can't be garbage collected - it sits around forever taking up space. Eventually you can run out of memory (or at least cause swapping and thrashing) because memory is full of objects nothing will ever use again. The most typical pattern for creating such leaks is adding something to a Collection somewhere and forgetting about it.

I've put together an example with a memory leak. First, read this code and see if you can find the leak:

public class MyFrame extends javax.swing.JFrame {
    MenuAction menuAction = new MenuAction();
    public MyFrame() {
        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setBounds (20, 20, 300, 300);
        getContentPane().addMouseListener (new MouseAdapter() {
            public void mouseReleased (MouseEvent me) {
                getPopupMenu().show((Component) me.getSource(), me.getX(), me.getY());
            }
        });
    }
   
    JPopupMenu getPopupMenu() {
        JPopupMenu menu = new JPopupMenu();
        menu.add (new JMenuItem (menuAction));
        return menu;
    }
   
    static final class MenuAction extends AbstractAction {
        public MenuAction() {
            putValue (Action.NAME, "Do Something");
        }
        public void actionPerformed (ActionEvent ae) {
            System.out.println("Action performed");
        }
    }
}

The above code contains a memory leak, but it's quite subtle. So lets assume we know that what is leaking is the JPopupMenu - we create a new one each time, and somehow they're not being garbage collected. So, what we'll do is write a regression test that fails - then we'll know when the bug is fixed. I've created a parent class for my unit test, called NbTestCase (get the original source which also contains assertions for data structure sizes here) - it's actually a stripped down version of a class with the same name which is part of XTest. It has a method assertGC() which we can use to assert that an object is garbage collected.

The usage pattern is simple: Null out any references in the test itself, after storing the object in question in a weak reference:

SomeObject o = new SomeObject();
something.doSomethingWith (o);
WeakReference ref = new WeakReference (o);
o = null;
assertGC ("Object still referenced", ref);
assertGC() forces the system to garbage collect several times, and if the object is still referenced from somewhere else, it shows what's holding on to it. The source is and INSANE lib are attached. So let's write our test:

public class MyFrameTest extends NbTestCase {
   
    public MyFrameTest(String testName) {
        super(testName);
    }

    MyFrame frame = null;
    protected void setUp() throws Exception {
        frame = new MyFrame();
        frame.setVisible (true);
        Thread.currentThread().sleep (1000);
    }

    public void testLeak() throws Exception {
        System.out.println("testLeak");
        JPopupMenu popup = frame.getPopupMenu();
        popup.show (frame, 0, 0);
        Thread.currentThread().sleep (1000);
        popup.setVisible (false);
       
        WeakReference ref = new WeakReference (popup);
        popup = null;
        assertGC ("Popup menu should have been collected", ref);
    }
   
    public static Test suite() {
        TestSuite suite = new TestSuite(MyFrameTest.class);
       
        return suite;
    }
}

This is where it becomes clear just how useful INSANE is - it gives us an actual reference graph, so we know precisely where the leak occured:

Testcase: testLeak(javaapplication7.MyFrameTest):	FAILED
Popup menu should have been collected:
private static java.awt.Component java.awt.KeyboardFocusManager.focusOwner->
javaapplication7.MyFrame@d1e89e->
javaapplication7.MyFrame$MenuAction@f17a73->
javax.swing.event.SwingPropertyChangeSupport@3526b0->
javax.swing.event.EventListenerList@3ddcf1->
[Ljava.lang.Object;@105c1->
javax.swing.JMenuItem$1@f74864->
javax.swing.JMenuItem@110003->
javax.swing.JPopupMenu@627086

So, our leak is simple: When you call new JMenuItem (someAction), the JMenuItem adds a property change listener to the action, to enable/disable itself. The listener is never removed, so each time a popup is shown, it hangs around as the parent of a JMenuItem which is held in the list property change listeners on the Action.

Now, I can think of about five ways to fix this bug, from obvious and straightforward to silly. Which way would you do it?