JDK-8158325 : Memory leak in com.apple.laf.ScreenMenu: removed JMenuItems are still referenced
  • Type: Bug
  • Component: client-libs
  • Sub-Component: javax.swing
  • Affected Version: 8u92,9
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: other
  • CPU: x86
  • Submitted: 2016-05-31
  • Updated: 2017-11-29
  • Resolved: 2016-06-30
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 8 JDK 9
8u152Fixed 9 b127Fixed
Description
FULL PRODUCT VERSION :
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
Darwin saotome.local 15.4.0 Darwin Kernel Version 15.4.0: Fri Feb 26 22:08:05 PST 2016; root:xnu-3248.40.184~3/RELEASE_X86_64 x86_64

A DESCRIPTION OF THE PROBLEM :
When using the system menu bar on OS X (System.setProperty("apple.laf.useScreenMenuBar", "true")), any JMenuItem you remove from a JMenu is still referenced as long as the menu bar exists.

The problem is located in the ScreenMenu class:

1) The ScreenMenu class attaches a ContainerListener to the invoker (=the JMenu instance) in the addNotify method . However, the JMenu never fires ContainerEvents. It is the JMenu#getPopupMenu that fires the events. As such, the cleanup code in com.apple.laf.ScreenMenu#componentRemoved is never triggered

2) In the com.apple.laf.ScreenMenu#componentRemoved, the entry from fItems is never removed, because the remove method is called with the value instead of the key

3) The com.apple.laf.ScreenMenu#updateItems method removes all entries through a removeAll call, but does not remove those entries from the fItems map (nor does it always clean the childHashArray, but that is no memory leak)

The attached test program illustrates that the JMenuItem, which has been removed from the JMenuBar, is still referenced.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the attached program

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Expected result is a successful termination of the program
ACTUAL -
A RuntimeException is thrown, because the JMenuItem is still referenced.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.awt.EventQueue;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.util.Objects;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.WindowConstants;

public class MenuBarMemoryLeakTest {
  private static byte[] sBytes;

  private static WeakReference<JMenuItem> sMenuItem;
  private static JFrame sFrame;
  private static JMenu sMenu;

  public static void main(String[] args) throws InvocationTargetException, InterruptedException {
    EventQueue.invokeAndWait(new Runnable() {
      @Override
      public void run() {
        System.setProperty("apple.laf.useScreenMenuBar", "true");
        showUI();
      }
    });

    EventQueue.invokeAndWait(new Runnable() {
      @Override
      public void run() {
        removeMenuItemFromMenu();
      }
    });
    fillUpMemory();
    JMenuItem menuItem = sMenuItem.get();
    EventQueue.invokeAndWait(new Runnable() {
      @Override
      public void run() {
        sFrame.dispose();
      }
    });
    if ( menuItem != null ){
      throw new RuntimeException("The menu item should have been GC-ed");
    }
  }

  private static void showUI(){
    sFrame = new JFrame();
    sFrame.add(new JLabel("Some dummy content"));

    JMenuBar menuBar = new JMenuBar();

    sMenu = new JMenu("Menu");
    JMenuItem item = new JMenuItem("Item");
    sMenu.add(item);

    sMenuItem = new WeakReference<>(item);

    menuBar.add(sMenu);

    sFrame.setJMenuBar(menuBar);
    sFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    sFrame.pack();
    sFrame.setVisible(true);
  }

  private static void removeMenuItemFromMenu(){
    JMenuItem menuItem = sMenuItem.get();
    Objects.requireNonNull(menuItem,"The menu item should still be available at this point");
    sMenu.remove(menuItem);
  }

  /**
   * Fill up the available heap space to ensure that any Soft and WeakReferences gets cleaned up
   */
  private static void fillUpMemory(){
    int size = 1000000;
    for (int i = 0; i < 50; i++) {
      System.gc();
      System.runFinalization();
      try {
        sBytes = null;
        sBytes = new byte[size];
        size = (int) (((double) size) * 1.3);
      } catch (OutOfMemoryError error) {
        size = size / 2;
      }
      try {
        if (i % 3 == 0) {
          Thread.sleep(321);
        }
      } catch (InterruptedException t) {
        // ignore
      }
    }
    sBytes = null;
  }
}

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

CUSTOMER SUBMITTED WORKAROUND :
Workaround is not using the system menu bar.


Comments
The proposed fix: http://cr.openjdk.java.net/~alexsch/robin.stevens/8158325/webrev.00 See discussion: http://mail.openjdk.java.net/pipermail/swing-dev/2016-June/006200.html
29-06-2016