JDK-4907798 : MEMORY LEAK: javax.swing.plaf.basic.BasicPopupMenuUI$MenuKeyboardHelper
  • Type: Bug
  • Component: client-libs
  • Sub-Component: javax.swing
  • Affected Version: 1.4.2,7,8,11,15,16
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: windows_2000
  • CPU: x86
  • Submitted: 2003-08-15
  • Updated: 2020-11-16
  • Resolved: 2020-11-11
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 16
16 b24Fixed
Description
Name: jk109818			Date: 08/15/2003


FULL PRODUCT VERSION :
java version "1.4.2"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.2-b28)
Java HotSpot(TM) Client VM (build 1.4.2-b28, mixed mode)

FULL OS VERSION :
Microsoft Windows 2000 [Version 5.00.2195]

A DESCRIPTION OF THE PROBLEM :
javax.swing.plaf.basic.BasicPopupMenuUI$MenuKeyboardHelper instances are not being removed from the static instance javax.swing.MenuSelectionManager.defaultManager() until after a new popup menu has been opened.  This is causing JFrames to not be garbage collected until after the user opens another popup menu which is very non-intuitive.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Open a JFrame which has a popupmenu (assume you have at least one other JFrame open).  Open the popupmenu.  Close the JFrame.  The JFrame instance will remain in memory until another popupmenu is activated.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The listener instances should have been de-registered when the popup window was closed (and likewise re-added when opened).
ACTUAL -
The JFrame remains in memory until another popupmenu is opened.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
Please run the following test and notice how many listeners are left when the SimpleUI.SwingHacks.CleanupJPopupMenuGlobals method is called.


public class SimpleUI extends javax.swing.JFrame
{
  /** Creates new form SimpleUI */
  public SimpleUI()
  {
    initComponents();
  }
  
  private void initComponents()
  {
    javax.swing.JMenu aJMenu_File = new javax.swing.JMenu("File");
    aJMenu_File.setMnemonic('F');

    javax.swing.Action aTestAction = new javax.swing.AbstractAction()
    {
      public void actionPerformed(java.awt.event.ActionEvent evt)
      {
        testPerformed(evt);
      }
    };
    aTestAction.putValue(aTestAction.NAME, "Test");
    aTestAction.putValue(aTestAction.MNEMONIC_KEY, new Integer((int) 'T'));
    aTestAction.putValue(aTestAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke("ctrl T"));

    javax.swing.JMenuItem aJMenuItem_Test = new javax.swing.JMenuItem(aTestAction);
    aJMenu_File.add(aJMenuItem_Test);

    javax.swing.JMenuBar aJMenuBar = new javax.swing.JMenuBar();
    aJMenuBar.add(aJMenu_File);
    
    setJMenuBar(aJMenuBar);

    final javax.swing.JPopupMenu aJPopupMenu = new javax.swing.JPopupMenu();
    javax.swing.JMenu aJMenu = new javax.swing.JMenu("File");
    javax.swing.JMenuItem aJMenuItem = new javax.swing.JMenuItem(aTestAction);
    aJMenu.add(aJMenuItem);
    aJPopupMenu.add(aJMenu);
    
    getContentPane().setLayout(new java.awt.FlowLayout());

    final javax.swing.JButton aJButton_Test = new javax.swing.JButton(aTestAction);
    aJButton_Test.addMouseListener(
      new java.awt.event.MouseAdapter()
      {
        public void mousePressed(java.awt.event.MouseEvent e)
        {
          if (e.isPopupTrigger())
            openJPopupMenu(e);
        }
        
        public void mouseReleased(java.awt.event.MouseEvent e)
        {
          if (e.isPopupTrigger())
            openJPopupMenu(e);
        }
        
        public void openJPopupMenu(java.awt.event.MouseEvent e)
        {
          aJPopupMenu.show(aJButton_Test, e.getX(), e.getY());
        }
      }
    );
    getContentPane().add(aJButton_Test);

    javax.swing.Action anAction_Spawn = new javax.swing.AbstractAction("Spawn")
    {
      public void actionPerformed(java.awt.event.ActionEvent evt)
      {
        spawnPerformed(evt);
      }
    };
    javax.swing.JButton aJButton_Spawn = new javax.swing.JButton(anAction_Spawn);
    getContentPane().add(aJButton_Spawn);

    javax.swing.Action anAction_Exit = new javax.swing.AbstractAction("Exit")
    {
      public void actionPerformed(java.awt.event.ActionEvent evt)
      {
        System.exit(0);
      }
    };
    javax.swing.JButton aJButton_Exit = new javax.swing.JButton(anAction_Exit);
    getContentPane().add(aJButton_Exit);

    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
  }
  
  public void show()
  {
    pack();
    
    addWindowListener(
      new java.awt.event.WindowAdapter()
      {
        public void windowClosed(java.awt.event.WindowEvent e)
        {
          e.getWindow().removeWindowListener(this);
          cleanup();
        }
        
        public void windowClosing(java.awt.event.WindowEvent e)
        {
          e.getWindow().removeWindowListener(this);
          dispose();
          cleanup();
        }
      }
    );

    super.show();
  }

  private void testPerformed(java.awt.event.ActionEvent evt)
  {
    setDefaultCloseOperation(super.DISPOSE_ON_CLOSE);
    dispose();

    spawnPerformed(evt);
  }
  
  private void spawnPerformed(java.awt.event.ActionEvent evt)
  {
    (new SimpleUI()).show();
  }

  public void cleanup()
  {
    SwingHacks.CleanupJPopupMenuGlobals(true);
    SwingHacks.CleanupJMenuBarGlobals();
  }

  /**
   * @param args the command line arguments
   */
  public static void main(String args[])
  {
    try
    {
      javax.swing.UIManager.setLookAndFeel(javax.swing.UIManager.getSystemLookAndFeelClassName());
    }
    catch (Exception e)
    {
    }
    new SimpleUI().show();
  }

  public static class SwingHacks
  {
    public static void CleanupJPopupMenuGlobals(boolean removeOnlyMenuKeyboardHelpers)
    {
      try
      {
        javax.swing.MenuSelectionManager aMenuSelectionManager = javax.swing.MenuSelectionManager.defaultManager();
        Object anObject = SafelyGetReflectedField("javax.swing.MenuSelectionManager", "listenerList", aMenuSelectionManager);
        if (null != anObject)
        {
          javax.swing.event.EventListenerList anEventListenerList = (javax.swing.event.EventListenerList) anObject;
          Object[] listeners = anEventListenerList.getListenerList();

          if (removeOnlyMenuKeyboardHelpers)
          {
            // This gives us back an Array and the even entries are the
            // class type.  In this case they are all javax.swing.event.ChangeListeners
            // The odd number entries are the instance themselves.
            // We were having a problem just blindly removing all of the listeners
            // because the next time a popupmenu was show, it wasn't getting dispose (i.e you
            // right click and click off to cancel and the menu doesn't go away).  We traced
            // the memory leak down to this javax.swing.plaf.basic.BasicPopupMenuUI$MenuKeyboardHelper
            // holding onto an instance of the JRootPane.  Therefore we just remove all of the
            // instances of this class and it cleans up fine and seems to work.
            Class aClass = Class.forName("javax.swing.plaf.basic.BasicPopupMenuUI$MenuKeyboardHelper");
            for (int i = listeners.length - 1; i >= 0; i -= 2)
            {
               if (aClass.isInstance(listeners[i]))
               {
                aMenuSelectionManager.removeChangeListener((javax.swing.event.ChangeListener) listeners[i]);
               }
            }
          }
          else
          {
            for (int i = listeners.length - 1; i >= 0; i -= 2)
            {
               aMenuSelectionManager.removeChangeListener((javax.swing.event.ChangeListener) listeners[i]);
            }
          }
        }
      }
      catch (Exception e)
      {
//        e.printStackTrace();
      }

      try
      {
        javax.swing.ActionMap anActionMap = (javax.swing.ActionMap) javax.swing.UIManager.getLookAndFeelDefaults().get("PopupMenu.actionMap");
        while (anActionMap != null)
        {
          Object[] keys = { "press", "release" };
          boolean anyFound = false;
          for (int i = 0; i < keys.length; i++)
          {
            Object aKey = keys[i];
            Object aValue = anActionMap.get(aKey);
            anyFound = anyFound || aValue != null;
            anActionMap.remove(aKey);
          }
          if (!anyFound)
          {
            break;
          }
          anActionMap = anActionMap.getParent();
        }
      }
      catch (Exception e)
      {
//        e.printStackTrace();
      }

      SafelySetReflectedFieldToNull("javax.swing.plaf.basic.BasicPopupMenuUI", "menuKeyboardHelper", null);

      Object anObject = SafelyGetReflectedField("com.sun.java.swing.plaf.windows.WindowsPopupMenuUI", "mnemonicListener", null);
      if (null != anObject)
      {
        SafelySetReflectedFieldToNull(anObject.getClass(), "repaintRoot", anObject);
      }

    }

    private static void SafelySetReflectedFieldToNull(Class aClass, String aFieldName, Object anObject)
    {
      try
      {
        java.lang.reflect.Field aField = aClass.getDeclaredField(aFieldName);
        aField.setAccessible(true);
        aField.set(anObject, null);
      }
      catch (Exception e)
      {
//        System.out.println(e)
      }
    }

    private static void SafelySetReflectedFieldToNull(String aClassName, String aFieldName, Object anObject)
    {
      try
      {
        Class aClass = Class.forName(aClassName);
        SafelySetReflectedFieldToNull(aClass, aFieldName, anObject);
      }
      catch (Exception e)
      {
//        System.out.println(e)
      }
    }

    private static Object SafelyGetReflectedField(String aClassName, String aFieldName, Object anObject)
    {
      try
      {
        Class aClass = Class.forName(aClassName);
        java.lang.reflect.Field aField = aClass.getDeclaredField(aFieldName);
        aField.setAccessible(true);
        return aField.get(anObject);
      }
      catch (Exception e)
      {
//        System.out.println(e)
        return null;
      }
    }

    public static void CleanupJMenuBarGlobals()
    {
      SafelySetReflectedFieldToNull("com.sun.java.swing.plaf.windows.WindowsRootPaneUI$AltProcessor", "root", null);
      SafelySetReflectedFieldToNull("com.sun.java.swing.plaf.windows.WindowsRootPaneUI$AltProcessor", "winAncestor", null);
    }
  }
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
The following code snipet seems to cleanup the instances correctly.  Set the boolean removeOnlyMenuKeyboardHelpers to true in all instances EXCEPT on the final window closing.

      try
      {
        javax.swing.MenuSelectionManager aMenuSelectionManager = javax.swing.MenuSelectionManager.defaultManager();
        Object anObject = SafelyGetReflectedField("javax.swing.MenuSelectionManager", "listenerList", aMenuSelectionManager);
        if (null != anObject)
        {
          javax.swing.event.EventListenerList anEventListenerList = (javax.swing.event.EventListenerList) anObject;
          Object[] listeners = anEventListenerList.getListenerList();

          if (removeOnlyMenuKeyboardHelpers)
          {
            // This gives us back an Array and the even entries are the
            // class type.  In this case they are all javax.swing.event.ChangeListeners
            // The odd number entries are the instance themselves.
            // We were having a problem just blindly removing all of the listeners
            // because the next time a popupmenu was show, it wasn't getting dispose (i.e you
            // right click and click off to cancel and the menu doesn't go away).  We traced
            // the memory leak down to this javax.swing.plaf.basic.BasicPopupMenuUI$MenuKeyboardHelper
            // holding onto an instance of the JRootPane.  Therefore we just remove all of the
            // instances of this class and it cleans up fine and seems to work.
            Class aClass = Class.forName("javax.swing.plaf.basic.BasicPopupMenuUI$MenuKeyboardHelper");
            for (int i = listeners.length - 1; i >= 0; i -= 2)
            {
               if (aClass.isInstance(listeners[i]))
               {
                aMenuSelectionManager.removeChangeListener((javax.swing.event.ChangeListener) listeners[i]);
               }
            }
          }
          else
          {
            for (int i = listeners.length - 1; i >= 0; i -= 2)
            {
               aMenuSelectionManager.removeChangeListener((javax.swing.event.ChangeListener) listeners[i]);
            }
          }
        }
      }
      catch (Exception e)
      {
//        e.printStackTrace();
      }
(Incident Review ID: 198820) 
======================================================================

Comments
Changeset: ed615e3c Author: Alexander Zuev <kizune@openjdk.org> Date: 2020-11-11 11:43:47 +0000 URL: https://github.com/openjdk/jdk/commit/ed615e3c
11-11-2020

I think the bug mentioned in the description still exists, it is possible to reproduce it by these steps(at least in WIndows L&F): 1. Override the finilize method in the SimpleUI class: protected void finalize() throws Throwable { System.out.println("Scratch.finalize"); } 2. Change implementation of aTestAction to this: javax.swing.Action aTestAction = new javax.swing.AbstractAction() { public void actionPerformed(java.awt.event.ActionEvent evt) { System.out.println("fill memory"); try { ArrayList gc = new ArrayList(); while (true) {gc.add(new int[100000]);} } catch (Throwable e){} System.out.println("done"); } }; 3. Run the SimpleUI test(the frame1 became visible) 4. Press the "Spawn" button(the frame2 became visible) 5. Open menu "File" in the frame2 6. Close frame2 7. Press "Test" button in the frame1 a few times(gc will be triggered a few times but the finalize() of the frame2 will not be called) 8. Open menu "File" in the frame1 9. Press "Test" button in the frame1(the finalize() of the frame2 will be called) It seems that the popup menu still prevents the frame to be deallocated.
05-10-2020

Code is changed since this issue was reported and this issue can no longer be reproduced since we do not keep references to the frame in the listener.
05-10-2020

I tried to reproduce this issue but it seems like the mail problem - references to a root pane and the frame itself from WindowsRootPaneUI$AltProcessor were fixed a long time ago (at least in jdk 1.5) and the other problem with KeyboardHelper registration also changed since jdk 1.4 and does not contain any references to the parent frame. Thus this issue can no longer manifest. Closing as not reproducible.
05-10-2020

[~kizune] Alex look like you already worked on this issue
25-01-2020

Please re-evaluate for JDK 9. The reflective workaround documented here uses reflection to access JDK-internal data. With Jigsaw this is no longer possible unless you also use an awkward command-line flag.
26-09-2016

EVALUATION Contribution forum : https://jdk-collaboration.dev.java.net/servlets/ProjectForumMessageView?forumID=1463&messageID=13415
08-06-2006

EVALUATION Contribution forum : https://jdk-collaboration.dev.java.net/servlets/ProjectForumMessageView?forumID=1463&messageID=13415
08-06-2006

EVALUATION We need to have popup menu to be gc'ed (see comments for more info)
06-04-2006

EVALUATION As this "Memory Leak" is not cumulative (the old instance will be released as soon as the any new popup appears) it cannot be named as leak. Closing this bug as not a bug. ###@###.### 2003-08-21
21-08-2003