JDK-6542439 : Significant memory leak in BasicComboBoxUI and MetalComboBoxButton
  • Type: Bug
  • Component: client-libs
  • Sub-Component: javax.swing
  • Affected Version: 5.0
  • Priority: P4
  • Status: Resolved
  • Resolution: Fixed
  • OS: windows_xp
  • CPU: x86
  • Submitted: 2007-04-04
  • Updated: 2024-11-20
  • Resolved: 2020-08-28
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 b14Fixed
Related Reports
Relates :  
Description
FULL PRODUCT VERSION :
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_10-b03)
Java HotSpot(TM) Server VM (build 1.5.0_10-b03, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
SunOS nys31a-016 5.8 Generic_117000-05 sun4u sparc SUNW,Sun-Fire-480R

A DESCRIPTION OF THE PROBLEM :
javax.swing.plaf.basic.BasicComboBoxUI and javax.swing.plaf.metal.MetalComboBoxButton use a CellRenderer pane to paint the renderer component retrieved via getListCellRenderer from the JList instance. CellRendererPane paints the component but it never removes it from its component hierarchy.  CellRendererPane does ADD the painted component automatically at the beginning of its paint method but _does_not  automatically remove the component at the end of its paint method - the component MUST be removed manually by the caller of the paint method.

This has HUGE impact. as JList can produce (and it does in our case) dynamic renderer component instances in getListCellRenderer method. Each one of these instances will linger in memory because it remains in the component hierarchy of the CellRendererPane.


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
BasicComboBoxUI.java

    public void paintCurrentValue(Graphics g,Rectangle bounds,boolean hasFocus) {
        ListCellRenderer renderer = comboBox.getRenderer();
        Component c;

        if ( hasFocus && !isPopupVisible(comboBox) ) {
            c = renderer.getListCellRendererComponent( listBox,
                                                       comboBox.getSelectedItem(),
                                                       -1,
                                                       true,
                                                       false );
        }
        else {
            c = renderer.getListCellRendererComponent( listBox,
                                                       comboBox.getSelectedItem(),
                                                       -1,
                                                       false,
                                                       false );
            c.setBackground(UIManager.getColor("ComboBox.background"));
        }
        c.setFont(comboBox.getFont());
        if ( hasFocus && !isPopupVisible(comboBox) ) {
            c.setForeground(listBox.getSelectionForeground());
            c.setBackground(listBox.getSelectionBackground());
        }
        else {
            if ( comboBox.isEnabled() ) {
                c.setForeground(comboBox.getForeground());
                c.setBackground(comboBox.getBackground());
            }
            else {
                c.setForeground(DefaultLookup.getColor(
                         comboBox, this, "ComboBox.disabledForeground", null));
                c.setBackground(DefaultLookup.getColor(
                         comboBox, this, "ComboBox.disabledBackground", null));
            }
        }

        // Fix for 4238829: should lay out the JPanel.
        boolean shouldValidate = false;
        if (c instanceof JPanel)  {
            shouldValidate = true;
        }

        currentValuePane.paintComponent(g,c,comboBox,bounds.x,bounds.y,
                                        bounds.width,bounds.height, shouldValidate);

///////////// BUG ///////// Component c is never removed from the currentValuePane
    }


MetalComboBoxButton.java


    public void paintComponent( Graphics g ) {
        boolean leftToRight = MetalUtils.isLeftToRight(comboBox);

        // Paint the button as usual
        super.paintComponent( g );

        Insets insets = getInsets();

        int width = getWidth() - (insets.left + insets.right);
        int height = getHeight() - (insets.top + insets.bottom);

        if ( height <= 0 || width <= 0 ) {
            return;
        }

        int left = insets.left;
        int top = insets.top;
        int right = left + (width - 1);
        int bottom = top + (height - 1);

        int iconWidth = 0;
        int iconLeft = (leftToRight) ? right : left;

        // Paint the icon
        if ( comboIcon != null ) {
            iconWidth = comboIcon.getIconWidth();
            int iconHeight = comboIcon.getIconHeight();
            int iconTop = 0;

            if ( iconOnly ) {
                iconLeft = (getWidth() / 2) - (iconWidth / 2);
                iconTop = (getHeight() / 2) - (iconHeight / 2);
            }
            else {
	        if (leftToRight) {
		    iconLeft = (left + (width - 1)) - iconWidth;
		}
		else {
		    iconLeft = left;
		}
                iconTop = (top + ((bottom - top) / 2)) - (iconHeight / 2);
            }

            comboIcon.paintIcon( this, g, iconLeft, iconTop );

            // Paint the focus
            if ( comboBox.hasFocus() && (!MetalLookAndFeel.usingOcean() ||
                                         comboBox.isEditable())) {
                g.setColor( MetalLookAndFeel.getFocusColor() );
                g.drawRect( left - 1, top - 1, width + 3, height + 1 );
            }
        }

        if (MetalLookAndFeel.usingOcean()) {
            // With Ocean the button only paints the arrow, bail.
            return;
        }

        // Let the renderer paint
        if ( ! iconOnly && comboBox != null ) {
            ListCellRenderer renderer = comboBox.getRenderer();
            Component c;
            boolean renderPressed = getModel().isPressed();
            c = renderer.getListCellRendererComponent(listBox,
                                                      comboBox.getSelectedItem(),
                                                      -1,
                                                      renderPressed,
                                                      false);
            c.setFont(rendererPane.getFont());

            if ( model.isArmed() && model.isPressed() ) {
                if ( isOpaque() ) {
                    c.setBackground(UIManager.getColor("Button.select"));
                }
                c.setForeground(comboBox.getForeground());
            }
            else if ( !comboBox.isEnabled() ) {
                if ( isOpaque() ) {
                    c.setBackground(UIManager.getColor("ComboBox.disabledBackground"));
                }
                c.setForeground(UIManager.getColor("ComboBox.disabledForeground"));
            }
            else {
                c.setForeground(comboBox.getForeground());
                c.setBackground(comboBox.getBackground());
            }


            int cWidth = width - (insets.right + iconWidth);
            
            // Fix for 4238829: should lay out the JPanel.
            boolean shouldValidate = false;
            if (c instanceof JPanel)  {
                shouldValidate = true;
            }
            
	    if (leftToRight) {
	        rendererPane.paintComponent( g, c, this,
					     left, top, cWidth, height, shouldValidate );
	    }
	    else {
	        rendererPane.paintComponent( g, c, this,
					     left + iconWidth, top, cWidth, height, shouldValidate );
	    }

//// BUG //// c is never removed from rendererPane
        }
    }

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The renderer component gets removed from the component hierarchy of the CellRendererPane after its painting is done. No memory leaks are introduced

ACTUAL -
ALL instances of a renderer linger in memory and are never garbage collected. JVM runs out of memory due to this memory leak.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
I provided your own source code pinpointing the problematic areas
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
BasicComboBoxUI.java

    public void paintCurrentValue(Graphics g,Rectangle bounds,boolean hasFocus) {
        ListCellRenderer renderer = comboBox.getRenderer();
        Component c;

        if ( hasFocus && !isPopupVisible(comboBox) ) {
            c = renderer.getListCellRendererComponent( listBox,
                                                       comboBox.getSelectedItem(),
                                                       -1,
                                                       true,
                                                       false );
        }
        else {
            c = renderer.getListCellRendererComponent( listBox,
                                                       comboBox.getSelectedItem(),
                                                       -1,
                                                       false,
                                                       false );
            c.setBackground(UIManager.getColor("ComboBox.background"));
        }
        c.setFont(comboBox.getFont());
        if ( hasFocus && !isPopupVisible(comboBox) ) {
            c.setForeground(listBox.getSelectionForeground());
            c.setBackground(listBox.getSelectionBackground());
        }
        else {
            if ( comboBox.isEnabled() ) {
                c.setForeground(comboBox.getForeground());
                c.setBackground(comboBox.getBackground());
            }
            else {
                c.setForeground(DefaultLookup.getColor(
                         comboBox, this, "ComboBox.disabledForeground", null));
                c.setBackground(DefaultLookup.getColor(
                         comboBox, this, "ComboBox.disabledBackground", null));
            }
        }

        // Fix for 4238829: should lay out the JPanel.
        boolean shouldValidate = false;
        if (c instanceof JPanel)  {
            shouldValidate = true;
        }

        currentValuePane.paintComponent(g,c,comboBox,bounds.x,bounds.y,
                                        bounds.width,bounds.height, shouldValidate);

// FIX // currentValuePane.remove(c);
    }


MetalComboBoxButton.java


    public void paintComponent( Graphics g ) {
        boolean leftToRight = MetalUtils.isLeftToRight(comboBox);

        // Paint the button as usual
        super.paintComponent( g );

        Insets insets = getInsets();

        int width = getWidth() - (insets.left + insets.right);
        int height = getHeight() - (insets.top + insets.bottom);

        if ( height <= 0 || width <= 0 ) {
            return;
        }

        int left = insets.left;
        int top = insets.top;
        int right = left + (width - 1);
        int bottom = top + (height - 1);

        int iconWidth = 0;
        int iconLeft = (leftToRight) ? right : left;

        // Paint the icon
        if ( comboIcon != null ) {
            iconWidth = comboIcon.getIconWidth();
            int iconHeight = comboIcon.getIconHeight();
            int iconTop = 0;

            if ( iconOnly ) {
                iconLeft = (getWidth() / 2) - (iconWidth / 2);
                iconTop = (getHeight() / 2) - (iconHeight / 2);
            }
            else {
	        if (leftToRight) {
		    iconLeft = (left + (width - 1)) - iconWidth;
		}
		else {
		    iconLeft = left;
		}
                iconTop = (top + ((bottom - top) / 2)) - (iconHeight / 2);
            }

            comboIcon.paintIcon( this, g, iconLeft, iconTop );

            // Paint the focus
            if ( comboBox.hasFocus() && (!MetalLookAndFeel.usingOcean() ||
                                         comboBox.isEditable())) {
                g.setColor( MetalLookAndFeel.getFocusColor() );
                g.drawRect( left - 1, top - 1, width + 3, height + 1 );
            }
        }

        if (MetalLookAndFeel.usingOcean()) {
            // With Ocean the button only paints the arrow, bail.
            return;
        }

        // Let the renderer paint
        if ( ! iconOnly && comboBox != null ) {
            ListCellRenderer renderer = comboBox.getRenderer();
            Component c;
            boolean renderPressed = getModel().isPressed();
            c = renderer.getListCellRendererComponent(listBox,
                                                      comboBox.getSelectedItem(),
                                                      -1,
                                                      renderPressed,
                                                      false);
            c.setFont(rendererPane.getFont());

            if ( model.isArmed() && model.isPressed() ) {
                if ( isOpaque() ) {
                    c.setBackground(UIManager.getColor("Button.select"));
                }
                c.setForeground(comboBox.getForeground());
            }
            else if ( !comboBox.isEnabled() ) {
                if ( isOpaque() ) {
                    c.setBackground(UIManager.getColor("ComboBox.disabledBackground"));
                }
                c.setForeground(UIManager.getColor("ComboBox.disabledForeground"));
            }
            else {
                c.setForeground(comboBox.getForeground());
                c.setBackground(comboBox.getBackground());
            }


            int cWidth = width - (insets.right + iconWidth);
            
            // Fix for 4238829: should lay out the JPanel.
            boolean shouldValidate = false;
            if (c instanceof JPanel)  {
                shouldValidate = true;
            }
            
	    if (leftToRight) {
	        rendererPane.paintComponent( g, c, this,
					     left, top, cWidth, height, shouldValidate );
	    }
	    else {
	        rendererPane.paintComponent( g, c, this,
					     left + iconWidth, top, cWidth, height, shouldValidate );
	    }

//fix/// rendererPane.remove(c);
        }
    }

Comments
URL: https://hg.openjdk.java.net/jdk/jdk/rev/c890a336bded User: psadhukhan Date: 2020-08-29 05:41:37 +0000
29-08-2020

URL: https://hg.openjdk.java.net/jdk/client/rev/c890a336bded User: psadhukhan Date: 2020-08-28 11:43:07 +0000
28-08-2020

EVALUATION A similar fix was already made for JList. See 5044798. Note that it's best to removeAll() after all painting is done, and not do single removes after each cell is rendered.
05-04-2007