-
Bug
-
Resolution: Fixed
-
P4
-
5.0
-
b14
-
x86
-
windows_xp
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);
}
}
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);
}
}
- relates to
-
JDK-5044798 Significant memory leak in BasicListUI: Renderer not removed from renderer pane
-
- Resolved
-