Living in a state of accord.

JApplet Memory Leaks

I mentioned the other day that I was having trouble with OutOfMemoryErrors being thrown when there shouldn't have been any references left around. We've done a fair bit more investigation into the cause of this and have wound up reporting a bug to Sun (still awaiting review so it's not in the public bug database yet). It turns out that you can cause OutOfMemoryErrors with an applet as simple as the one below if you just refresh the page a few times – but apparently only if you're using a dual-cpu or dual-core system.

import javax.swing.*;
public class TinyApplet extends JApplet {
    private byte[] data = new byte[Integer.MAX_VALUE / 100];

If however, you extends from Applet instead of JApplet, the OutOfMemoryErrors suddenly disappear. Tipped off by a comment in the source code for JApplet:

       /* Workaround for bug 4155072. The shared double buffer image
       * may hang on to a reference to this applet; unfortunately
       * Image.getGraphics() will continue to call JApplet.getForeground()
       * and getBackground() even after this applet has been destroyed.
       * So we ensure that these properties are non-null here.

Bug 4155072 in turn explains that when using Component.createImage a reference to the Component is held so that the image can get the foreground and background colors. Sadly, Component.createImage is used to support double buffering in swing and so JApplets aren't correctly garbage collected. The bug seems to be a lot worse in Java 1.6 but bug 4155072 dates back to Java 1.2 and we've seen problems with Applet rention back to at least Java 1.3 but previously we'd believed it was the Sun plugin or the browser holding onto the applet.

Fortunately there's a simple work around, you just need to get the RepaintManager to discard the offscreen buffer. RepaintManager.setDoubleBufferingEnabled(false) doesn't actually let go of any existing buffers, but the code below does, while still preserving double buffering support in the applet so you don't see refresh flickering.

RepaintManager manager = RepaintManager.currentManager(theApplet);
Rectangle maxDoubleBufferSize = manager.getDoubleBufferMaximumSize();
manager.setDoubleBufferMaximumSize(new Dimension(-1, -1));

By setting the maximum size to -1, every buffer in the RepaintManager is too big and gets flushed out, later we set the size back so that double buffering is still enabled. No more memory leak. Actually, still a memory leak. Not sure what's going on there – if you disable double buffering all together it goes away but for some reason setting the maximum double buffer size isn't clearing things out like it should.

UPDATE 2: Setting the maximum double buffer size doesn't work, but setting the current repaint manager to null seems to. I won't proclaim it as a fix quite so confidently this time until we've finished testing but it seems to solve the problem.

The most annoying part of this is that back in Java 1.2 Sun applied a work around for bug 4155072 and left the memory leak in there instead of actually fixing the underlying problem. On the plus side, I'll have the chance to hunt down the culprit in a couple of weeks at JavaOne.

Category: Java