JDK-6469530 : Memory leak in the focus subsystem
  • Type: Bug
  • Component: client-libs
  • Sub-Component: java.awt
  • Affected Version: 6
  • Priority: P3
  • Status: Closed
  • Resolution: Fixed
  • OS: generic
  • CPU: generic
  • Submitted: 2006-09-12
  • Updated: 2013-11-01
  • Resolved: 2011-05-17
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 7
7 b03Fixed
Related Reports
Relates :  
Relates :  
Relates :  
Relates :  
Relates :  
Description
I've found what looks like a long-standing memory leak involving the AWT focus subsystem.  I believe this is the root cause of bug 6462383 for 1.4.2 and 5.0, and possibly 6.0.

6462383 reports that even after a JFrame has been disposed and has no Java references, it is clearly still taking up space in the heap.  I was able to get YourKit to tell me that the frame was being kept alive via a JNI GlobalRef.  Some further debugging hinted that the most likely spot for the GlobalRef to be coming from was focus-related code.  In particular, awt_Component.cpp contains the following methods:

void *
AwtComponent::GetNativeFocusOwner() {
    JNIEnv *env = (JNIEnv *)JNU_GetEnv(jvm, JNI_VERSION_1_2);
    AwtComponent *comp =
        AwtComponent::GetComponent(AwtComponent::sm_focusOwner);
    return (comp != NULL) ? comp->GetTargetAsGlobalRef(env) : NULL;
}
void *
AwtComponent::GetNativeFocusedWindow() {
    JNIEnv *env = (JNIEnv *)JNU_GetEnv(jvm, JNI_VERSION_1_2);
    AwtComponent *comp =
        AwtComponent::GetComponent(AwtComponent::sm_focusedWindow);
    return (comp != NULL) ? comp->GetTargetAsGlobalRef(env) : NULL;
}

which both call GetTargetAsGlobalRef() in awt_Object.h:

    INLINE jobject GetTargetAsGlobalRef(JNIEnv *env) {
        jobject localRef = GetTarget(env);
        if (localRef == NULL) {
            return NULL;
        }

        jobject globalRef = env->NewGlobalRef(localRef);
        env->DeleteLocalRef(localRef);
        return globalRef;
    }

The functions above certainly show the possibilty for GlobalRefs to be created and then not be deleted.

I wrote an AWT-only test case which demonstrates this bug.  It should be run with -verbose:gc to show the memory leak when a normal Frame is created and disposed of.  Additionally, you can also see that an unfocusable Frame does not exhibit this leak - after a GC or two, the unfocusable Frame's memory drops out of the heap.

SimpleAWTTest.java:
-------------------
import java.awt.event.*;
import java.awt.*;

public class SimpleAWTTest extends Frame implements ActionListener {
    Button gcBtn;
    Button bigBtn;
    Button dspBtn;

    Button bigUnfBtn;
    Button dspUnfBtn;

    Frame bigFrame;
    Frame bigUnfFrame;

    public SimpleAWTTest() {
        super("SimpleAWTTest");
        Panel btnPnl = new Panel();
        btnPnl.setLayout(new FlowLayout());

        gcBtn = new Button("GC");
        gcBtn.addActionListener(this);
        btnPnl.add(gcBtn);

        bigBtn = new Button("Create Frame");
        bigBtn.addActionListener(this);
        btnPnl.add(bigBtn);

        dspBtn = new Button("Dispose Frame");
        dspBtn.addActionListener(this);
        btnPnl.add(dspBtn);

        bigUnfBtn = new Button("Create Unfocusable Frame");
        bigUnfBtn.addActionListener(this);
        btnPnl.add(bigUnfBtn);

        dspUnfBtn = new Button("Dispose Unfocusable Frame");
        dspUnfBtn.addActionListener(this);
        btnPnl.add(dspUnfBtn);

        add(btnPnl, BorderLayout.SOUTH);
        addWindowListener(new WL());
    }

    public void actionPerformed(ActionEvent e) {
        Object src = e.getSource();
        if (src == gcBtn) {
            System.gc();
        }
        else if (src == bigBtn) {
            bigFrame = new LeakFrame(true);
        }
        else if (src == dspBtn) {
            if (bigFrame != null) {
                bigFrame.dispose();
            }
            bigFrame = null;
        }
        else if (src == bigUnfBtn) {
            bigUnfFrame = new LeakFrame(false);
        }
        else if (src == dspUnfBtn) {
            if (bigUnfFrame != null) {
                bigUnfFrame.dispose();
            }
           bigUnfFrame = null;
        } 
    }

    static class LeakFrame extends Frame {
        byte[] bigLeak;

        public LeakFrame(boolean focusable) {
            super("Big Frame");
            bigLeak = new byte[1024 * 1024 * 24];
            setFocusableWindowState(focusable);
            if (focusable) {
                setBounds(0, 100, 100, 100);
            }
            else {
                setBounds(150, 100, 100, 100);
            }
            setVisible(true);
        }

        public void dispose() {
            System.out.println("dispose() called");
            super.dispose();
        }
    }
    
    static class WL implements WindowListener {
               public  void 	windowClosed(WindowEvent e) {
        }
        public  void 	windowActivated(WindowEvent e) {
        }
        public  void 	windowClosing(WindowEvent e){
            System.out.println("closing");
            Window src = ((Window)e.getSource());
            src.setVisible(false);
            src.dispose();
        }
        public  void 	windowDeactivated(WindowEvent e){}
        public  void 	windowDeiconified(WindowEvent e){}
        public  void 	windowIconified(WindowEvent e){}
        public  void 	windowOpened(WindowEvent e) {} 
    }

    public static void main(String[] args) {
        SimpleAWTTest f = new SimpleAWTTest();
        f.pack();
        f.setVisible(true);
    }
}

FWIW, this bug can be hard to pickup with a profiler.  It appears that there could be garbage collector action that (rarely) will pick up the wayward GlobalRef, though it's quite unreliable, and doesn't change the fact that we're creating GlobalRefs that don't get deleted.

Though long-standing, this has the potential to be a serious memory leak, depending on an application's architecture.  It should be fixed in 7.0 ASAP, and backported to all active update releases.
BTW, the test case is only equipped to handle 1 single focusable/non-focusable window at a time. :)

Comments
SUGGESTED FIX +++ DefaultKeyboardFocusManager.java 2006-10-13 14:06:04.000000000 +0400 @@ -10,10 +10,11 @@ import java.awt.event.KeyEvent; import java.awt.event.WindowEvent; import java.awt.peer.ComponentPeer; import java.awt.peer.LightweightPeer; import java.beans.PropertyChangeListener; +import java.lang.ref.WeakReference; import java.util.LinkedList; import java.util.Iterator; import java.util.ListIterator; import java.util.Set; @@ -44,12 +45,17 @@ * @since 1.4 */ public class DefaultKeyboardFocusManager extends KeyboardFocusManager { private static final Logger focusLog = Logger.getLogger("java.awt.focus.DefaultKeyboardFocusManager"); - private Window realOppositeWindow; - private Component realOppositeComponent; + // null weak references to not create too many objects + private static final WeakReference<Window> NULL_WINDOW_WR = + new WeakReference<Window>(null); + private static final WeakReference<Component> NULL_COMPONENT_WR = + new WeakReference<Component>(null); + private WeakReference<Window> realOppositeWindow = NULL_WINDOW_WR; + private WeakReference<Component> realOppositeComponent = NULL_COMPONENT_WR; private int inSendMessage; private LinkedList enqueuedKeyEvents = new LinkedList(), typeAheadMarkers = new LinkedList(); private boolean consumeNextKeyTyped; @@ -81,11 +87,11 @@ * This series of restoreFocus methods is used for recovering from a * rejected focus or activation change. Rejections typically occur when * the user attempts to focus a non-focusable Component or Window. */ private void restoreFocus(FocusEvent fe, Window newFocusedWindow) { - Component realOppositeComponent = this.realOppositeComponent; + Component realOppositeComponent = this.realOppositeComponent.get(); Component vetoedComponent = fe.getComponent(); if (newFocusedWindow != null && restoreFocus(newFocusedWindow, vetoedComponent, false)) { @@ -96,11 +102,11 @@ } else { clearGlobalFocusOwner(); } } private void restoreFocus(WindowEvent we) { - Window realOppositeWindow = this.realOppositeWindow; + Window realOppositeWindow = this.realOppositeWindow.get(); if (realOppositeWindow != null && restoreFocus(realOppositeWindow, null, false)) { } else if (we.getOppositeWindow() != null && restoreFocus(we.getOppositeWindow(), null, false)) { } else { @@ -354,11 +360,11 @@ // was inactive it expects to receive focus after activation. toFocus.requestFocusInWindow(CausedFocusEvent.Cause.ACTIVATION); } } - Window realOppositeWindow = this.realOppositeWindow; + Window realOppositeWindow = this.realOppositeWindow.get(); if (realOppositeWindow != we.getOppositeWindow()) { we = new WindowEvent(newFocusedWindow, WindowEvent.WINDOW_GAINED_FOCUS, realOppositeWindow); } @@ -487,11 +493,11 @@ } } setNativeFocusOwner(getHeavyweight(newFocusOwner)); - Component realOppositeComponent = this.realOppositeComponent; + Component realOppositeComponent = this.realOppositeComponent.get(); if (realOppositeComponent != null && realOppositeComponent != fe.getOppositeComponent()) { fe = new CausedFocusEvent(newFocusOwner, FocusEvent.FOCUS_GAINED, fe.isTemporary(), @@ -544,11 +550,12 @@ setNativeFocusOwner(null); fe.setSource(currentFocusOwner); realOppositeComponent = (fe.getOppositeComponent() != null) - ? currentFocusOwner : null; + ? new WeakReference<Component>(currentFocusOwner) + : NULL_COMPONENT_WR; return typeAheadAssertions(currentFocusOwner, fe); } case WindowEvent.WINDOW_DEACTIVATED: { @@ -628,12 +635,12 @@ } setNativeFocusedWindow(null); we.setSource(currentFocusedWindow); realOppositeWindow = (oppositeWindow != null) - ? currentFocusedWindow - : null; + ? new WeakReference<Window>(currentFocusedWindow) + : NULL_WINDOW_WR; typeAheadAssertions(currentFocusedWindow, we); if (oppositeWindow == null) { // Then we need to deactive the active Window as well. // No need to synthesize in other cases, because
18-10-2006

EVALUATION it is realOppositeComponent and realOppositeWindow fields of DKFM who keeps strong references to disposed window/component. It looks like that the easiest/best way to fix this is to convert these fileds into weak references.
13-10-2006

EVALUATION We only call GetNativeFocusOwner() and GetNativeFocusedWindow() from KFM::GetNativeFocusState\90 which always deletes global ref as soon as it receives (actually the only need of the globar ref here is to pass reference from one thread to another.) Thus I doubt that the cause of the leak we see is in that code.
27-09-2006

WORK AROUND When running my test case, I've been able to workaround this bug by giving focus to some other application (I usually use a native terminal window), then back to the test case.
14-09-2006

EVALUATION An interesting update: by giving focus to some other window (I used a native app), the leak will go away. Do the following with the test case: *Create Focusable Frame *Dispose Focusable Frame At this point, you can keep GCing and GCing and the leak will persist. But as soon as you give focus to some other window, when you refocus the test window, GCing will clean up the heap.
12-09-2006

EVALUATION Along with returning a GlobalRef, AwtObject::GetTargetAsGlobalRef() could also store the global ref in an instance variable. When the destructor is called, the GlobalRef could be deleted.
12-09-2006