Last active
June 21, 2021 06:54
-
-
Save stanio/065c7051a835a2d76cd805f60685162b to your computer and use it in GitHub Desktop.
MultiResolutionToolkitImage.ObserverCache memory leak
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package net.example.swing; | |
import java.awt.BasicStroke; | |
import java.awt.BorderLayout; | |
import java.awt.Color; | |
import java.awt.Component; | |
import java.awt.Container; | |
import java.awt.Font; | |
import java.awt.FontMetrics; | |
import java.awt.Graphics2D; | |
import java.awt.Image; | |
import java.awt.LayoutManager; | |
import java.awt.RenderingHints; | |
import java.awt.event.ActionEvent; | |
import java.awt.image.BufferedImage; | |
import java.awt.image.ImageObserver; | |
import java.lang.ref.Reference; | |
import java.lang.ref.ReferenceQueue; | |
import java.lang.ref.WeakReference; | |
import java.lang.reflect.InvocationTargetException; | |
import java.util.ArrayList; | |
import java.util.concurrent.Executors; | |
import java.util.concurrent.ScheduledExecutorService; | |
import java.util.concurrent.TimeUnit; | |
import javax.swing.AbstractAction; | |
import javax.swing.Box; | |
import javax.swing.BoxLayout; | |
import javax.swing.DefaultListModel; | |
import javax.swing.Icon; | |
import javax.swing.ImageIcon; | |
import javax.swing.JCheckBox; | |
import javax.swing.JComponent; | |
import javax.swing.JFrame; | |
import javax.swing.JLabel; | |
import javax.swing.JList; | |
import javax.swing.JPanel; | |
import javax.swing.JScrollPane; | |
import javax.swing.JSplitPane; | |
import javax.swing.JTabbedPane; | |
import javax.swing.JToolBar; | |
import javax.swing.LookAndFeel; | |
import javax.swing.SwingUtilities; | |
/** | |
* <em>Not an actual leak!</em> Soft references tend to stick around for much | |
* longer than weak references, for example. Try using VM arguments like:</p> | |
* <pre> | |
* -XX:SoftRefLRUPolicyMSPerMB=1 -Xmx64M</pre> | |
* <p> | |
* to cause soft references to be cleared faster.</p> | |
* | |
* @see <a href="https://bugs.openjdk.java.net/browse/JDK-8257500">JDK-8257500 : Drawing MultiResolutionImage with ImageObserver leaks memory</a> | |
* @see <a href="https://bugs.openjdk.java.net/browse/JDK-8022449">JDK-8022449 : Can we get rid of sun.misc.SoftCache?</a> | |
*/ | |
@SuppressWarnings("serial") | |
public class MrImageObserverLeakTest extends JFrame { | |
DefaultListModel<Reference<Object>> liveObjects; | |
ReferenceQueue<Object> queue = new ReferenceQueue<>(); | |
JTabbedPane tabs; | |
JCheckBox workaroundDefault; | |
JCheckBox workaroundDisabled; | |
AbstractAction openTab = new AbstractAction("+Tab") { | |
{ | |
super.putValue(SHORT_DESCRIPTION, "Open a New Tab (Ctrl+T)"); | |
} | |
@Override public void actionPerformed(ActionEvent evt) { | |
TabContent content = new TabContent(workaroundDefault.isSelected(), | |
workaroundDisabled.isSelected()); | |
tabs.addTab(content.getName(), null, content, "Ctrl+W to Close"); | |
liveObjects.addElement(new DebugReference(content, queue)); | |
if ((evt.getModifiers() & ActionEvent.SHIFT_MASK) == 0) { | |
tabs.setSelectedIndex(tabs.getTabCount() - 1); | |
} | |
} | |
}; | |
AbstractAction closeTab = new AbstractAction() { | |
@Override | |
public void actionPerformed(ActionEvent evt) { | |
if ((evt.getModifiers() & ActionEvent.SHIFT_MASK) == 0) { | |
int index = tabs.getSelectedIndex(); | |
if (index >= 0) tabs.removeTabAt(index); | |
} else { | |
tabs.removeAll(); | |
} | |
} | |
}; | |
public MrImageObserverLeakTest() { | |
super("Multi-Resolution ImageObserver Leak Test"); | |
Container contentPane = super.getContentPane(); | |
contentPane.add(initToolBar(), BorderLayout.PAGE_START); | |
contentPane.add(initMainPanel(), BorderLayout.CENTER); | |
} | |
private Component initToolBar() { | |
JToolBar toolBar = new JToolBar(); | |
toolBar.setName("Tools"); | |
toolBar.setFloatable(false); | |
toolBar.setRollover(true); | |
toolBar.add(openTab); | |
toolBar.add(new AbstractAction("GC") { | |
{ | |
super.putValue(SHORT_DESCRIPTION, "Run the Garbage Collector (Ctrl+Click for Aggressive)"); | |
} | |
@Override public void actionPerformed(ActionEvent evt) { | |
if ((evt.getModifiers() & ActionEvent.CTRL_MASK) != 0) { | |
try { | |
// java.util.ArrayList.MAX_ARRAY_SIZE | |
// jdk.internal.util.ArraysSupport.MAX_ARRAY_LENGTH | |
final int maxArraySize = Integer.MAX_VALUE - 8; | |
final ArrayList<Object> temp = new ArrayList<>(); | |
long freeBytes; | |
while ((freeBytes = Runtime.getRuntime().freeMemory()) > 0) { | |
int size = (int) Math.min(freeBytes, maxArraySize); | |
temp.add(new byte[size]); | |
} | |
} catch (OutOfMemoryError e) { | |
// -XX:SoftRefLRUPolicyMSPerMB=... Soft references are | |
// more likely to be cleared sooner at this point. | |
} | |
} | |
System.gc(); | |
} | |
}); | |
toolBar.addSeparator(); | |
toolBar.add(new JLabel("Icon workaround:")).setEnabled(false); | |
workaroundDefault = (JCheckBox) toolBar.add(new JCheckBox("\"Default\"", true)); | |
workaroundDefault.setOpaque(false); | |
workaroundDisabled = (JCheckBox) toolBar.add(new JCheckBox("\"Disabled\"", true)); | |
workaroundDisabled.setOpaque(false); | |
return toolBar; | |
} | |
private Component initMainPanel() { | |
tabs = new JTabbedPane(); | |
tabs.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); | |
liveObjects = new DefaultListModel<>(); | |
JList<?> objectList = new JList<Reference<Object>>(liveObjects) { | |
@Override public boolean getScrollableTracksViewportWidth() { return true; } | |
}; | |
tabs.getActionMap().put("OpenTab", openTab); | |
tabs.getActionMap().put("CloseTab", closeTab); | |
LookAndFeel.loadKeyBindings(tabs.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW), | |
new Object[] { "control T", "OpenTab", | |
"control shift T", "OpenTab", | |
"control W", "CloseTab", | |
"control shift W", "CloseTab" }); | |
JScrollPane listScroll = new JScrollPane(objectList); | |
Box listPane = Box.createVerticalBox(); | |
JLabel listLabel = (JLabel) listPane.add(new JLabel("Live objects:")); | |
listLabel.setLabelFor(listScroll); | |
listScroll.setAlignmentX(LEFT_ALIGNMENT); | |
listPane.add(listScroll); | |
JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, tabs, listPane); | |
split.setResizeWeight(0.4); | |
SwingUtilities.invokeLater(() -> split.setDividerLocation(0.4)); | |
return split; | |
} | |
void startPolling() { | |
ScheduledExecutorService service = Executors.newScheduledThreadPool(1); | |
Runnable pollQueue = () -> { | |
try { | |
Reference<?> ref; | |
while ((ref = queue.poll()) != null) { | |
final Reference<?> refCapture = ref; | |
SwingUtilities.invokeAndWait(() -> { | |
int index = liveObjects.indexOf(refCapture); | |
assert (index >= 0); | |
liveObjects.removeElementAt(index); | |
}); | |
} | |
} catch (InvocationTargetException e) { | |
System.err.println(e); | |
} catch (InterruptedException e) { | |
System.err.println(e); | |
Thread.currentThread().interrupt(); | |
} catch (OutOfMemoryError e) { | |
// Try again next time | |
} | |
}; | |
service.scheduleAtFixedRate(pollQueue, 5, 1, TimeUnit.SECONDS); | |
} | |
public static void main(String[] args) throws Exception { | |
SwingUtilities.invokeLater(() -> { | |
// swing.defaultlaf, swing.metalTheme | |
if (!System.getProperties().containsKey("swing.metalTheme")) { | |
// The default Ocean doesn't have MultiResolutionImage | |
// support with LookAndFeel.getDisabledIcon() | |
System.setProperty("swing.metalTheme", "steel"); | |
} | |
MrImageObserverLeakTest frame = new MrImageObserverLeakTest(); | |
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); | |
frame.pack(); | |
frame.setLocation(50, 50); | |
frame.setVisible(true); | |
frame.startPolling(); | |
}); | |
} | |
static class TabContent extends JPanel { | |
private static int count = 0; | |
private final boolean workaroundDefault; | |
private final boolean workaroundDisabled; | |
TabContent(boolean workaroundDefault, boolean workaroundDisabled) { | |
super((LayoutManager) null); | |
super.setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS)); | |
super.setName("#" + (count += 1)); | |
this.workaroundDefault = workaroundDefault; | |
this.workaroundDisabled = workaroundDisabled; | |
super.add(Box.createHorizontalGlue()); | |
ImageIcon defaultIcon = TestIcon.createMrIcon(); | |
if (workaroundDefault) { | |
TestIcon.avoidMrLeak(defaultIcon); | |
} | |
super.add(new LeakedComponent(defaultIcon)); | |
super.add(Box.createHorizontalStrut(5)); | |
JLabel disabled = (JLabel) super.add(new LeakedComponent(defaultIcon)); | |
Icon disabledIcon = disabled.getDisabledIcon(); | |
if (workaroundDisabled) { | |
TestIcon.avoidMrLeak(disabledIcon); | |
} | |
disabled.setEnabled(false); | |
super.add(Box.createHorizontalGlue()); | |
} | |
@Override | |
protected String paramString() { | |
return getName() + ", workaround: default=" + workaroundDefault + ", disabled=" + workaroundDisabled; | |
} | |
} | |
static class DebugReference extends WeakReference<Object> { | |
DebugReference(Object referent, ReferenceQueue<? super Object> q) { | |
super(referent, q); | |
} | |
@Override | |
public String toString() { | |
Object t = get(); | |
if (t == null) { | |
return "null"; | |
} | |
return t.toString(); | |
} | |
} | |
static class DummyObserver implements ImageObserver { | |
static final ImageObserver INSTANCE = new DummyObserver(); | |
@Override | |
public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { | |
return false; | |
} | |
} | |
static class TestIcon { | |
static ImageIcon createMrIcon() { | |
final int userWidth = 48; | |
final int userHeight = 48; | |
// Using Java-internal class just for test/demo purpose. | |
Image mrImage = new sun.awt.image.MultiResolutionCachedImage(userWidth, userHeight, | |
(deviceWidth, deviceHeight) -> { | |
BufferedImage variant = new BufferedImage(deviceWidth, deviceHeight, BufferedImage.TYPE_INT_ARGB); | |
Graphics2D g = variant.createGraphics(); | |
g.scale((double) deviceWidth / userWidth, (double) deviceHeight / userHeight); | |
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, | |
RenderingHints.VALUE_ANTIALIAS_ON); | |
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, | |
RenderingHints.VALUE_TEXT_ANTIALIAS_ON); | |
final int padding = 2; | |
g.setColor(Color.blue); | |
g.fillOval(padding, padding, userWidth - 2 * padding, userHeight - 2 * padding); | |
g.setColor(Color.white); | |
g.setStroke(new BasicStroke(2)); | |
g.drawOval(padding, padding, userWidth - 2 * padding, userHeight - 2 * padding); | |
g.setFont(new Font(Font.SERIF, Font.BOLD, 40)); | |
FontMetrics fm = g.getFontMetrics(); | |
int stringWidth = fm.stringWidth("i"); | |
int stringHeight = 27; // magic | |
g.drawString("i", (userWidth - (float) stringWidth) / 2, | |
(userHeight + (float) stringHeight) / 2); | |
g.dispose(); | |
return variant; | |
}); | |
return new ImageIcon(mrImage); | |
} | |
static <T extends Icon> T avoidMrLeak(T icon) { | |
if (icon instanceof ImageIcon) { | |
((ImageIcon) icon).setImageObserver(DummyObserver.INSTANCE); | |
} | |
return icon; | |
} | |
} | |
static class LeakedComponent extends JLabel { | |
LeakedComponent(Icon icon) { | |
super(icon); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Use
--add-opens java.desktop/sun.awt.image=ALL-UNNAMED
JVM option to prevent: